Create a simple Note-taking app with Deno
Jeferson Brito
Posted on May 20, 2020
Since Deno 1.0 version was released last week it started to really catch the attention of all of us from the JavaScript Community, especially on the server-side of the ecosystem.
For those who don't know, Deno is a secure runtime for TypeScript and JavaScript, it was invented by the same creator of Node.js, Ryan Dahl.
It is written in TypeScript and Rust and built on top of V8 Engine.
In this tutorial, we're going to learn Deno by building a simple command-line interface for taking notes. We're going to go through his Standard Modules like File System operations (Read and Write JSON files) and Third-party modules to create commands and interact with the terminal.
So without further do, Let's get started
Installation
Shell (macOS, Linux)
$ curl -fsSL https://deno.land/x/install/install.sh | sh
Powershell (Windows)
$ iwr https://deno.land/x/install/install.ps1 -useb | iex
Homebrew (macOS)
$ brew install deno
Chocolatey (Windows)
$ choco install deno
We can test if the Deno is successfully installed by running this example app in your command line:
deno run https://deno.land/std/examples/welcome.ts
Application structure
.
├── data
│ └── notes-data.json
└── src
├── app.ts
└── notes.ts
First off, let's create our initial JSON file containing our notes,
open notes-data.json
and write the following:
[
{
"title": "Note one",
"body": "Go to the Grocery Store"
},
{
"title": "Note two",
"body": "Watch a TV Show"
}
]
Now we switch to our src
folder and open app.ts
to bootstrap our application:
// Thirty-party modules
import Denomander from 'https://deno.land/x/denomander/mod.ts';
// Local imports
import * as notes from './notes.ts';
const program = new Denomander({
app_name: "Deno Notes App",
app_description: "Create notes in json format from the command line",
app_version: "1.0.0",
});
We are using a third-party module called Denomander, it's pretty much like commander.js, we will use it to create commands for us to run in the terminal.
Writing commands
After declaring our program we're going to implement five commands:
...
// Add command
program
.command("add")
.description("Add a new note")
.action(() => {
const title = prompt("Note title:") ?? "Note three";
const body = prompt("Note body:") ?? "";
notes.createNote({ title, body });
});
// List command
program
.command("list")
.description("List all notes")
.action(() => {
notes.listNotes();
});
// Read command
program
.command("read")
.description("Read a note")
.action(() => {
const title = prompt("Note title: ");
notes.readNote(title);
});
// Update command
program
.command("update")
.description("Update a note")
.action(() => {
const existingNote = prompt(
"What note do you want to update? [title]",
) as string;
const title = prompt("New title:") ?? "Note one";
const body = prompt("New body:") ?? "";
notes.updateNote(existingNote, { title, body });
});
// Remove command
program
.command("remove")
.description("Remove a note")
.action(() => {
const title = prompt("Note title:");
notes.removeNote(title);
});
program.parse(Deno.args);
Deno 1.5 introduced prompt
API to interact with input from the user, so our application is now able to respond to list, add, read, update and remove commands.
Writing operations
Then we can implement each separately, so let's write some I/O operations:
Open notes.ts
file and import the following modules:
// Standard deno modules
import * as path from "https://deno.land/std/path/mod.ts";
// Thirty party modules
import iro, {
bgGreen,
bold,
inverse,
red,
yellow,
} from "https://deno.land/x/iro/src/iro.ts";
const currentDir = Deno.cwd();
const notesFilePath = path.resolve(`${currentDir}/data/notes-data.json`);
path is a file system standard module that we're going to use to manipulate file paths and directories . If you know some of Node.js, you'll notice that it's pretty similar to the path
module.
iro is a third-party terminal coloring and styles utility module.
Now Let's implement our first operations
...
interface Note {
title: string;
body: string;
}
export async function fetchNotes() {
try {
const file = await Deno.readTextFile(notesFilePath);
const notes: Note[] = JSON.parse(file);
return notes;
} catch (error) {
console.error(error);
return [];
}
}
export async function listNotes() {
const notesList: Note[] = await fetchNotes();
console.log(iro(" Your notes ", inverse));
for (const note of notesList) {
console.log(" - ", note.title);
console.log("●".padStart(5), note.body);
}
}
export async function saveNotes(notes: Note[]) {
try {
await Deno.writeTextFile(notesFilePath, JSON.stringify(notes));
} catch (error) {
throw new Error(`Unable to write contents to file: ${error}`);
}
}
Our app is going to fetch our initial notes, and then perform operations to list and save notes.
Deno's runtime API provides the Deno.readTextFile
and Deno.writeTextFile
asynchronous functions for reading and writing entire files as text files.
Moving on, with these methods we are able to create and read commands:
export async function createNote({ title, body }: Note) {
const notesList = await fetchNotes();
const isDuplicate = notesList.find((note: Note) => note.title === title);
if (!isDuplicate) {
notesList.push({ title, body });
await saveNotes(notesList);
console.log(iro("New note added!", bold, bgGreen));
} else {
console.log(iro("Note title already taken!", inverse, red));
}
}
export async function readNote(noteTitle: string) {
const notesList = await fetchNotes();
const searchedNote = notesList.find((note: Note) => {
return note.title.toLocaleLowerCase() === noteTitle.toLocaleLowerCase();
});
if (searchedNote) {
console.log(iro(searchedNote.title, inverse));
console.log(searchedNote.body);
} else {
console.log(iro("Note not found!", bold, inverse, red));
}
}
Finally, we implement the last two I/O operations for updating and removing our notes.
export async function removeNote(title: string) {
const notesList = await fetchNotes();
const notesToKeep = notesList.filter(
(note: Note) => note.title.toLowerCase() !== title.toLowerCase(),
);
if (notesList.length > notesToKeep.length) {
await saveNotes(notesToKeep);
console.log(iro("Note removed!", bgGreen));
} else {
console.log(iro("No note found!", inverse, yellow));
}
}
export async function updateNote(note: string, { title, body }: Partial<Note>) {
const notesList = await fetchNotes();
const currentNote = notesList.find(
(n: Note) => n.title.toLowerCase() === note.toLowerCase(),
);
const newNote = { title, body } as Note;
if (currentNote) {
notesList.splice(notesList.indexOf(currentNote), 1, newNote);
await saveNotes(notesList);
console.log(iro("Note updated!", bgGreen));
} else {
console.log(iro("This note does not exists", inverse, yellow));
}
}
Our application now can remove and update notes based on the title.
Experimenting
Last but not least, we can run our program by entering one of these commands
$ deno run --unstable --allow-write --allow-read src/app.ts add
// or
$ deno run --unstable -A src/app.ts add
Note title: Note three
Note body: This a new note
By the time of this writing, some of these APIs are still experimental, so we need a --unstable
flag to run it.
Deno does not provide default access to files, so you need to explicitly define the flags to read and write.
$ deno run --unstable --allow-read src/app.ts read
Note title: Note 3
- Note 3
● my new note
We also use --allow-read to list our notes:
$ deno run --allow-read src/app.ts list
- Note one
● Go to the Grocery Store
- Note two
● Watch a TV Show
- Note 3
● my new note
You can see all the commands and the source code of this application in this repo.
That's all folks!
This my very first post written entirely in English, So I'd love to hear your opinion, if you have any questions please leave a comment in the section below.
Posted on May 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.