Your First Deno Server in 60 Lines

nas5w

Nick Scialli (he/him)

Posted on May 18, 2020

Your First Deno Server in 60 Lines

Today we're going to write our first Deno server in 60 lines. Deno is self-described as a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust." I'm a huge fan of typescript, so I was really excited to hear about a runtime that treated typescript as a first-class citizen!

Learn More

If you like this post, consider checking out my free mailing list and YouTube tutorials to learn more JavaScript- and Typescript-related things!

Getting Started

First, we have to install the runtime. There are a lot of OS-dependent ways to do this, so I'm going to refer you to the Deno docs to get it installed.

Once Deno is installed, you should be able to type deno --version into your command line and see something like this:

deno 1.0.0
v8 8.4.300
typescript 3.9.2
Enter fullscreen mode Exit fullscreen mode

Deno is young and moving fast, so I wouldn't be surprised if you have a newer version!

Specifying our Domain

For our first server, let's pretend we're maintaining some sort of virtual bookshelf. Our domain, therefore, deals with books. We can use typescript to specify our Book type and create an array of books with an initial item. Let's create this file in a new directory and call the file server.ts:

server.ts

type Book = {
  id: number;
  title: string;
  author: string;
};

const books: Book[] = [
  {
    id: 1,
    title: "The Hobbit",
    author: "J. R. R. Tolkien",
  },
];
Enter fullscreen mode Exit fullscreen mode

Grabbing a Server Library

The oak server library appears to be, thus far, the most ubiquitous server library for deno. Let's use it!

If you're familiar with node, you might think we use an install command and maintain our version in some kind of package.json-like file. Not so! Instead, we specify the package url in our import statement and pin down the version within the import. Deno will first see if we have a cached version of the resource and, if not, will fetch and cache it.

Importantly, note that we specify version 4.0.0 of oak. If we don't specify the version, we'll just get the latest! Seems dangerous given the possibility of breaking changes along the way.

We're going to import Application and Router from oak. These will create our app server and allow us to configure routes, respectively.

We can add a get route to our root url to respond with "Hello world!" We tell our app to listen on port 8000.

import { Application, Router } from "https://deno.land/x/oak@v4.0.0/mod.ts";

const app = new Application();
const router = new Router();

router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })

app.use(router.routes());

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode

This is a functioning server, so we should test it out! In the directory with your file, run the following command:

deno run --allow-net server.ts
Enter fullscreen mode Exit fullscreen mode

Your app is now listening on port 8000, so you should be able to navigate to http://localhost:8000 in your browser and see our Hello World example!

Add Our Routes

We can now add some routes! I will set up some common CRUD routes on our book resource: get book to see all books, get book:id to see a specific book, and post book to create a book.

import { Application, Router } from "https://deno.land/x/oak@v4.0.0/mod.ts";

type Book = {
  id: number;
  title: string;
  author: string;
};

const books: Book[] = [
  {
    id: 1,
    title: "The Hobbit",
    author: "J. R. R. Tolkien",
  },
];

const app = new Application();

const router = new Router();

router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
  .get("/book", (context) => {
    context.response.body = books;
  })
  .get("/book/:id", (context) => {
    if (context.params && context.params.id) {
      const id = context.params.id;
      context.response.body = books.find((book) => book.id === parseInt(id));
    }
  })
  .post("/book", async (context) => {
    const body = await context.request.body();
    if (!body.value.title || !body.value.author) {
      context.response.status = 400;
      return;
    }
    const newBook: Book = {
      id: 2,
      title: body.value.title,
      author: body.value.author,
    };
    books.push(newBook);
    context.response.status = 201;
  });

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

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode

I think the only bit of this code that might be new or unexplained is app.use(router.allowedMethods());. This is simply a handy middleware that will let clients know when a route method is not allowed!

Final Touch: Logging Middleware

Let's add one final touch: logging middleware that logs how long each request takes:

import { Application, Router } from "https://deno.land/x/oak@v4.0.0/mod.ts";

type Book = {
  id: number;
  title: string;
  author: string;
};

const books: Book[] = [
  {
    id: 1,
    title: "The Hobbit",
    author: "J. R. R. Tolkien",
  },
];

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`);
});

const router = new Router();

router
  .get("/", (context) => {
    context.response.body = "Hello world!";
  })
  .get("/book", (context) => {
    context.response.body = books;
  })
  .get("/book/:id", (context) => {
    if (context.params && context.params.id) {
      let id = context.params.id;
      context.response.body = books.find((book) => book.id === parseInt(id));
    }
  })
  .post("/book", async (context) => {
    const body = await context.request.body();
    if (!body.value.title || !body.value.author) {
      context.response.status = 400;
      return;
    }
    const newBook: Book = {
      id: 2,
      title: body.value.title,
      author: body.value.author,
    };
    books.push(newBook);
    context.response.status = 201;
  });

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

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode

Now whenever you hit our server, the route path and the amount of time it takes to send a response will be logged to the console.

Fin

And there you have it! Our first Deno server in 60 lines. I'm a huge fan of Deno and looking forward to learning more about it as it evolves. I do have some questions and concerns (for example, given the lack of a lockfile, I'm wondering if and how Deno will allow developers control over indirect dependencies), but for now I'm just enjoying tinkering with this new toy.

Learn More

If you like this post, consider checking out my free mailing list and YouTube tutorials to learn more JavaScript- and Typescript-related things!

💖 💪 🙅 🚩
nas5w
Nick Scialli (he/him)

Posted on May 18, 2020

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

Sign up to receive the latest update from our blog.

Related