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 callingaddUser
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
Top comments (0)