DEV Community

Cover image for Building a restful API using Deno (Part2)
Vinicius Cerqueira Bonifácio
Vinicius Cerqueira Bonifácio

Posted on

Building a restful API using Deno (Part2)

Hi again, dear devs.

Hoping that is everything fine with each one of you. :)

Welcome to the Part 2 of our series. It supposed to be the last one but I didn't expected I would take that much explaining the controllers. My sincere apologies about that 🙏 but I do prefer posting content in a way easy to understand.

Just to recap, we already created both server and app files, separated in their own modules but our project doesn't do anything yet because there are neither routes nor controllers.

I will use my approach creating and implementing them because that it is the logic order that I understand better (it is neither the only one nor the best).

So, take your coffee mug, open your favorite lo-fi hip-hop Spotify's playlist and letys getty ztarted !

app.js

Let's import the routes in our app.js file so our app can make use of them.

1 import { Application } from "https://deno.land/x/oak/mod.ts";
2 import router from "./routes/routes.js";
3
4 const app = new Application();
5
6 app.use(router.routes());
7
8 /** You can set the allowed methods here*/
9 app.use(router.allowedMethods());
10 
11 export default app;
Enter fullscreen mode Exit fullscreen mode

Basically, we import our routes from ./routes/routes.js on line 2 (the file doesn´t exist yet) and explicitly set the app to use the routes (on line 6). Also we set the router to allow all methods allowMethods() (on line 9).
As commented in the code, we can choose which methods we allow our application to run, for instance, we could have restricted your app to use the delete method.

routes.js

Some developers declare the routes and controllers together in the same file (generally inside of the controllers file) but personally I prefer to divide them in their own modules.
Let's take a look in the code below:

NB: As you have been noticed, routes.js file will be under the "./routes/" directory. Obviously you can put it anywhere you wish but please remember to correct the importing path.

1  import { Router } from "https://deno.land/x/oak/mod.ts";
2  import {
3   getBooks,
4   getBook,
5   addBook,
6   removeBook,
7   updateBook,
8  } from "../controllers/books.js";
9
10 const baseURL = "/javascript/books";
11 
12 const router = new Router();
13 
14 router
15   .get(`${baseURL}`, getBooks)
16   .get(`${baseURL}/:id`, getBook)
17   .post(`${baseURL}`, addBook)
18   .delete(`${baseURL}/:id`, removeBook)
19   .put(`${baseURL}/:id`, updateBook);
20
21 export default router;
Enter fullscreen mode Exit fullscreen mode

Line 1: We imported the oak router middleware for Deno.

Line 2: We imported the controllers from book.js file. (They don't exist yet)

Line 10: We created a base url to avoid typing it every time we create an endpoint. (Programmers are lazy, do you remember it? :) )

Line 12: We created a new router and assigned it to the router variable. (Sorry for commenting the obvious)

Line 14: We defined a set of request methods with their respective endpoints which will interact with the incoming HTTP requests. (Wow, it sounded fancy I guess.)

Line 21: Remember that previously in the app.js file we imported the routes without even had created the route.js file yet? (This time I swear I won't comment the obvious again 😛.)

Well done! We have just finished creating our routes. Next step, the so-called controllers.

books.js

The file will be placed on "/controllers/" directory.

SPOILER ALERT: such an overwhelming file at first glance but don't let it intimidate you. Those functions there follow basically the same concept.

import books from "../data.js";
import { v4 } from "https://deno.land/std/uuid/mod.ts";

// METHOD: GET
// ROUTE: /javascript/books
// DESCRIPTION: Brings all books
const getBooks = ({ response }) => {
  response.status = 200;
  response.body = books;
};

// METHOD: GET
// ROUTE: /javascript/books/:id
// DESCRIPTION: Brings book by id
const getBook = ({ params, response }) => {
  const book = books.filter((book) => book.id.toString() === params.id);

  if (!book.length) {
    response.status = 404;
    response.body = { message: `Book with "id: ${params.id}" not found.` };
    return;
  }

  response.status = 200;
  response.body = book;
  return;
};

// METHOD: POST
// ROUTE: /javascript/books/
// DESCRIPTION: Adds a new book
const addBook = async ({ request, response }) => {
  const body = await request.body();

  if (!request.hasBody) {
    response.status = 400;
    response.body = {
      message: "Something went wrong. Try add a new book later.",
    };
    return;
  }

  const book = body.value;
  book.id = v4.generate();
  books.push(book);

  response.status = 201;
  response.body = book;
};

// METHOD: DELETE
// ROUTE: /javascript/books/:name
// DESCRIPTION: Removes a book from the list
const removeBook = async ({ params, response }) => {
  /** Returns a new array filtered without the book with id equals to params.id */
  const booksFiltered = books.filter((book) =>
    book.id.toString() !== params.id.toString()
  );

  /** If length of both arrays are equals we assume that no deletion happened so
   * we return as response that the book was note found in the list */
  if (booksFiltered.length === books.length) {
    response.status = 400;
    response.body = {
      message: `Book with ID ${params.id} Not Found On Books List`,
    };
    return;
  }

  response.body = {
    message: "Successfully Deleted",
    booksFiltered,
  };

  response.status = 200;
};

// METHOD: PUT
// ROUTE: /javascript/books/:name
// DESCRIPTION: Updates a book from the list
const updateBook = async ({ params, request, response }) => {
  /** Filters the books list using the params.id  */
  const bookToBeUpdated = books.filter((book) =>
    book.id.toString() === params.id.toString()
  );

  const body = await request.body();
  /*  Destructures the request body to update only the sent book fields*/
  const { title, author, url } = body.value;

  /** If after filter the books' array a book was found, updates it */
  if (bookToBeUpdated.length) {
    title ? bookToBeUpdated[0].title = title : bookToBeUpdated[0].title;
    author ? bookToBeUpdated[0].author = author : bookToBeUpdated[0].author;
    url ? bookToBeUpdated[0].url = url : bookToBeUpdated[0].url;

    response.status = 200;
    response.body = {
      message: `Book ${title} Sucessfully Updated`,
      bookToBeUpdated,
    };

    return;
  }

  /** If No Book Was Found Returns proper status code and message*/
  response.status = 400;
  response.body = {
    message: `Book With ID ${params.id} Was Not Found On Book List`,
  };
};

export { getBooks, getBook, addBook, removeBook, updateBook };
Enter fullscreen mode Exit fullscreen mode

Thinking about closing the tab already? Don't do it now. I'll teach you something that I learned from my past experiences and it may help you in your future projects. I call it:

Lego Driven Development

You didn't expected for that, right? 😂

But, what the heck does it mean, man? Please, allow me to explain it. Look to the image below:

Dinosaur built using Lego pieces

It is amazing, isn't it? Well, it was built using pieces like these, one at time.

Lego Bricks

The bottom line is that when something looks like too complex, try to break it into small pieces.

books.js but this time broken in pieces

Brick one (The GET all books method)

1 import books from "../data.js";
2 import { v4 } from "https://deno.land/std/uuid/mod.ts";
3
4 // METHOD: GET
5 // ROUTE: /javascript/books
6 // DESCRIPTION: Brings all books
7 const getBooks = ({ response }) => {
8   response.status = 200;
9   response.body = books;
10 };
11 ...
Enter fullscreen mode Exit fullscreen mode

Line 1: We imported the file where our hard-coded data is stored. (Guess what? We didn't created it yet 😂 but we will very soon)

Line 2: We imported a middleware to generate random ids (We'll use it later on next episode)

Lines 4, 5 , 6: I have been following the good practice of always describe functions. In this case, which HTTP method it uses, which endpoint it has and what it does. Believe me, it can save you even hours when reading someone's else code.

Lines 7, 8, 9, 10: It doesn't differ too much from a simple controller function if you are used to develop REST APIs. Just for one tiny detail that is worth pointing out: ({response})

In short, controller functions receive a context object as standard parameter, for example:

const getBooks = (context) => { 
  // ...
 })
Enter fullscreen mode Exit fullscreen mode

We just destructured it using the destructuring assignment syntax.

Lines 7, 8, 9, 10: When the application reaches this endpoint "/javascript/books" the response body will be the status code 200 (The request has succeeded) and the books from our hard-coded data. Talking about that, let's create it now so we can finally see something in the browser.

data.js

In the root of your project, you can copy & paste the content below. (By the way, this is the only moment you are allowed to copy & paste. 😎)

let books = [
  {
    id: 1,
    title: "Secrets of the JavaScript Ninja",
    author: "John Resig",
    url: "https://johnresig.com/",
  },

  {
    id: 2,
    title: "JavaScript: The Good Parts",
    author: "Douglas Crockford",
    url: "https://www.crockford.com/blog.html",
  },
  {
    id: 3,
    title: "You Don't Know JS Yet",
    author: "Kyle Simpson",
    url: "https://youdontknowjs.com",
  },
  {
    id: 4,
    title: "Eloquent JavaScript",
    author: "Marijn Haverbeke",
    url: "https://eloquentjavascript.net/author",
  },
  {
    id: 5,
    title: "Javascript For Kids",
    author: "Chris Minnick",
    url: "http://www.chrisminnick.com/blog/",
  },
];

export default books;
Enter fullscreen mode Exit fullscreen mode

Alright! Now you can import the data.js in your controllers file, start the Deno server using the appropriated flags (Do you still remember it?) ...

deno run --allow-read --allow-net
Enter fullscreen mode Exit fullscreen mode

... and check your browser at. It should render all the books from the data.js file.

As I said in the beginning, I will break this part in one more piece so I can explain calmly the remaining controllers methods (GET by id, PUT, DELETE and POST).

Thanks for reading and I hope to have you again on next episode. :)

NB: You can find the previous posts from those series in the links below: 👇🏽

Deno is not here to replace Node

Building a restful API using Deno (Intro)

Building a restful API using Deno (Part1)

Top comments (0)