Build RESTful API Server with Deno and Docker

This article originally posted on 345tool blog

Deno 1.0 has been released in May, 2020. Deno is a modern runtime that uses V8 and is built in Rust. Compared to its predecessor Node.js, Deno ships with a set of standard modules and secure by default. Although there is still a long way to go, Deno gives the TypeScript users a solid option to build modern web applications. In this article, we are going to build a simple RESTful API server with Deno, and containerize it with Docker.

TLDR: If you prefer reading the code, you can find the working code here and the version of Deno is 1.2

What we are going to build

We are going to build a very typical RESTful API server, it will do CRUD operations on backend data based on the requests received. The server contains following logic:

User Service - a service to manipulate users data. For demonstrate purposes, the service will only manipulate the data stored in memory.

API Server - a Deno http server listens. This is the main part we going to build. When the server started, it exposes two API endpoints GET /users and POST /user.

Docker Image - finally we are going to containerize deno runtime and our application using docker.

User service

To keep it simple, we use a list of users to mock the database data, and the addUser method will insert a new user into the list, the listUsers method will return the users list. We will call these methods in server request handlers.

export interface User {
  id: number;  name: string;
}

const users: User[] = [
  { id: 1, name: "John" },
  { id: 2, name: "Emily" },
  { id: 3, name: "Kevin" },
];

export const listUsers = (): User[] => {
  return users;
};

export const addUser = (user: User): void => {
  users.push(user);
};

Deno Server

To create a http server in Deno, we are going to use the standard http module

// index.ts
import { serve, Server } from "https://deno.land/std/http/server.ts";

const app: Server = serve({ port: 4040 });
console.log('server started on localhost:4040')

Deno allows us import modules directly from remote url, when the code been executed at first time, Deno will download those remote modules and cache them in local, so next time we can run the code without internet access. Now let's run the code by executing

deno --allow-net index.ts

Notice --allow-net flag, it will enable the network access for Deno runtime, otherwise it will be failed to initiate the server. After running, we can see the ouput

Download https://deno.land/std/http/server.ts
server started on localhost:4040

So far, we have create a very basic http server without any functions, next we will make the server to listen for http requests and make responds. Deno implements the http server in a very elegant way, the Server class implements AsyncIterable<ServerRequest> interface, which allows us to use the for await syntax to iterate over the http requests asynchronously. You can read more about AsyncIterable in this awesome article. Lets add following lines

import { serve, Server } from "https://deno.land/std/http/server.ts";

const app: Server = serve({ port: 4040 });

console.log('server started on localhost:4040');

for await (const req of app) {
  req.respond({
    status: 200,
    body: "Hello World!",
  });
}

Now we have a http server which will respond Hello World! for each incoming request! Next, we are going to define the Restful APIs, we will handle following requests:

  • POST /user add a new user by calling addUser in API handler
  • GET /users respond with a list of users
  • For other requests, we will respond 404 error

Let's add more logic in for await block

import { serve, Server, ServerRequest } from "https://deno.land/std/http/server.ts";
import { listUsers, addUser, User } from "./user-service.ts";
import { respondNotFound, respondWithBody, parseRequestBody } from "./utilities.ts";

const app: Server = serve({ port: 4040 });

console.log('server started on localhost:4040')

for await (const req of app) {
  switch (req.url) {
    case "/user": {
      switch (req.method) {
        case "POST": {
          const newUser = await parseRequestBody<User>(req);
          addUser(newUser);
          respondWithBody(req, true);
          break;
        }
        default:
          respondNotFound(req);
      }
      break;
    }
    case "/users": {
      switch (req.method) {
        case "GET": {
          respondWithBody<User[]>(req, listUsers());
          break;
        }
        default:
          respondNotFound(req);
      }
      break;
    }
    default:
      respondNotFound(req);
  }
}

When a request coming, we use switch to check the req.url and req.method properties, and we only handle the request which both url and method matched our API defines, otherwise we call respondNotFound to throw 404 error. As you can see, it is a pretty basic way to triage incoming requests, as the number of APIs growing, for separation of concerns, we can split the code in switch block by implementing some middlewares.

Finally, let's take a look at the request handlers, parseRequestBody will read the request body binary and convert it to json object.

const parseRequestBody = async <T>(req: ServerRequest): Promise<T> => {
  const buf = new Uint8Array(req.contentLength || 0);
  let bufSlice = buf;
  let totRead = 0;
  while (true) {
    const nread = await req.body.read(bufSlice);
    if (nread === null) break;
    totRead += nread;
    if (totRead >= req.contentLength!) break;
    bufSlice = bufSlice.subarray(nread);
  }
  const str = new TextDecoder("utf-8").decode(bufSlice);
  return JSON.parse(str) as T;
};

respondWithBody and respondNotFound are two helper methods to send API respond

const respondNotFound = (req: ServerRequest) =>
  req.respond({
    status: 404,
    body: "request not found!",
  });

const respondWithBody = <T>(req: ServerRequest, body?: T) => {
  const headers = new Headers();
  headers.append("Content-Type", "application/json");

  req.respond({
    status: 200,
    headers,
    body: JSON.stringify(body),
  });
};

Docker image

To run our app in docker, first install Deno runtime and setup the path. Finally make sure the app running with --allow-net and --allow-read flags to have correct permissions

# Dockerfile
FROM ubuntu:18.04

COPY . server/

RUN apt-get update && apt-get install -y sudo; \
    sudo apt-get update && sudo apt-get -y install curl unzip; \
    curl -fsSL https://deno.land/x/install/install.sh | sh;

WORKDIR server/

EXPOSE 4040

ENV DENO_INSTALL="/root/.deno"

ENV PATH="$DENO_INSTALL/bin:$PATH"

CMD deno run --allow-net --allow-read index.ts

Learn Something New Everyday,
Connect With The Best Developers!

Sign Up Now!

& 500k+ others use Hashnode actively.

No Comments Yet