How to create your first REST API with Deno

In this post I will explain step by step how to create a REST API with Deno.

There is no need to explain the steps to install Deno. They are very well described on the official website. For this example I have used VS Code, for which this official plugin exists.

What is Deno?

Deno is very similar to Node, but what Deno is trying to do is to have a safer runtime and correcting the base errors that Node had. Deno is safe by default. This means that it does not have access to your hard drive, nor to your network. He will have it only if you give it to him. Node, on the other hand, has access to practically everything as soon as it is installed.

For example, if we follow the official guide and execute the first command, we will see that it works without issues.

deno run https://deno.land/std/examples/welcome.ts

But if we do it with the example that follows:

import { serve } from "https://deno.land/[email protected]/http/server.ts";
const s = serve({ port: 8000 });

console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

We will encounter a network access permission error:

Compile file://welcome.ts
error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10)
    at Object.listen ($deno$/ops/net.ts:51:10)
    at listen ($deno$/net.ts:152:22)
    at serve (https://deno.land/[email protected]/http/server.ts:261:20)
    at file://welcome.ts:2:11

This happens because the first example does not need any additional access to work, but the second needs access to our network. Deno, having security by default, does not allow us to do it unless we grant extra permission to do so.

In this example, you can run it with the –allow-net parameter, and it will work.

The same goes for files, in this case we need –allow-read

Create an API with Deno

What a better way to start playing with Deno than by creating our first REST API. 😅

With this little tutorial I am going to create a very simple array of movies and the 5 methods to list, search, create, update, and delete elements.

The first step is to create an index file. In this case app.ts. The first thing will be to load Oak, a middleware framework for Deno’s HTTP server. Oak is inspired by Koa, a middleware for Node.js. It seems that they continue with the pun. In the end, it helps us make writing APIs easier.

It is a fairly simple example that is practically self-explanatory. The server will listen on port 4000 and load the routes defined in the router.ts file that we will see right after. In the file ./api/controller.ts I will put the definition of the functions for the different endpoints.

import { Application } from "https://deno.land/x/oak/mod.ts";
import router from "./router.ts";
import {
  getMovies,
  getMovie,
  createMovie,
  updateMovie,
  deleteMovie,
} from "./api/controller.ts";

const env = Deno.env.toObject();
const HOST = env.HOST || "127.0.0.1";
const PORT = env.PORT || 4000;

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

console.log(`API is listening on ${HOST}:${PORT}...`);
await app.listen(`${HOST}:${PORT}`);

It’s time to define the routes in the router.ts file. Here we will also import the Oak Router and the definitions that we will create in the controller.ts

We instantiate a Router and define the 5 commented routes.

Method Function
getMovies Returns all the movies
getMovie Returns a movie given it’s ID
createMovie Creates a new movie
updateMovie Updates an existing movie
deleteMovie Deletes a movie
import { Router } from "https://deno.land/x/oak/mod.ts";
import {
  getMovies,
  getMovie,
  createMovie,
  updateMovie,
  deleteMovie,
} from "./api/controller.ts";

const router = new Router();

router
  .get("/movies", getMovies)
  .get("/movie/:id", getMovie)
  .post("/movies", createMovie)
  .put("/movies/:id", updateMovie)
  .delete("/movies/:id", deleteMovie);

export default router;

Now it’s time to create the controller.ts file to define the API methods and the with the test database.

interface Movie {
  id: string;
  title: string;
  rating: number;
}

Then, the movies array.

/**
 * Sample array with movies
 */
let movies: Array<Movie> = [
  {
    id: "1",
    title: "TENET",
    rating: 10,
  },
  {
    id: "2",
    title: "No Time to Die",
    rating: 8,
  },
  {
    id: "3",
    title: "The Way Back",
    rating: 7,
  },
  {
    id: "4",
    title: "The Invisible Man",
    rating: 9,
  },
  {
    id: "5",
    title: "Onward",
    rating: 8,
  },
];

And now, the different methods starting with the one that lists all the movies. It is really that simple:

/**
 * Returns all the movies in database
 */
const getMovies = ({ response }: { response: any }) => {
  response.body = movies;
};

Let’s go to the next one, the one in charge of returning a movie from an ID that we can pass as a parameter.

/**
 * Returns a movie by id
 */
const getMovie = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  const movie = movies.filter((movie) => movie.id == params.id)[0];
  if (movie) {
    response.status = 200;
    response.body = movie;
  } else {
    response.status = 404;
    response.body = { message: "404 Not found" };
  }
};

If we try to launch the request with Postman, we will see that it works.

image 12
Lanzamos la petición para un ID en concreto.

It is the turn of the createMovie method to create a movie. The code is the following:

/**
 * Creates a new movie
 */
const createMovie = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) => {
  const body = await request.body();
  const movie: Movie = body.value;
  movies.push(movie);
  response.body = { success: true, data: movie };
  response.status = 201;
};

If we launch the test request, the server will reply with a message containing the recently created movie data.

image 14

If we then launch the request to return all the movies, we will see how the new one appears correctly.

image 15

It is the turn of the updateMovie method to update a movie. The code is:

/**
 * Updates an existing movie
 */
const updateMovie = async ({
  params,
  request,
  response,
}: {
  params: { id: string };
  request: any;
  response: any;
}) => {
  const movie = movies.filter((movie) => movie.id == params.id)[0];
  if (movie) {
    const body = await request.body();
    movie.title = body.value.title;
    movie.rating = body.value.rating;
    response.status = 200;
    response.body = {
      success: true,
      data: movies,
    };
  } else {
    response.status = 404;
    response.body = {
      success: false,
      message: "Movie not found",
    };
  }
};

We launch the corresponding PUT request with Postman, and we will get the correct response.

image 17

And finally, we only have the deleteMovie method that, in this case, deletes a movie from a given id. What I do is use the filter () to update the array keeping all the movies with a different id than the one sent.

/**
 * Deletes a movie by a given id
 */
const deleteMovie = ({
  params,
  response,
}: {
  params: { id: string };
  response: any;
}) => {
  movies = movies.filter((movie) => movie.id !== params.id);
  response.status = 200;
  response.body = { success: true, message: "Movie removed" };
};

We try with Postman …

image 19

And effectively the movie with id = 1 just disappeared 😎

image 21

You can download all the source code for this example in this repository from my GitHub.