Create a simple Note-taking app with Deno

jeferson_sb

Jeferson Brito

Posted on May 20, 2020

Create a simple Note-taking app with Deno

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
Enter fullscreen mode Exit fullscreen mode

Powershell (Windows)

$ iwr https://deno.land/x/install/install.ps1 -useb | iex
Enter fullscreen mode Exit fullscreen mode

Homebrew (macOS)

$ brew install deno
Enter fullscreen mode Exit fullscreen mode

Chocolatey (Windows)

$ choco install deno
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Application structure

.
├── data
│   └── notes-data.json
└── src
    ├── app.ts
    └── notes.ts
Enter fullscreen mode Exit fullscreen mode

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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`);
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
jeferson_sb
Jeferson Brito

Posted on May 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related