Hello deno: Building a minimal HTTP Server

Hello deno: Building a minimal HTTP Server
TL;DR: Check the GitHub repo with the code I implemented here.

Everyone —really, everyone— is talking about Deno right now. As of today, the Javascript runtime has reached the 1.0.0 version (a.k.a. the stable one), there are still a lot of missing characteristics to work in a production environment the way we currently do.

And while their features might indicate there is a potential for a lot of real use cases (applications with Electron, React Native, lambda functions, etc.), I'm still a bit skeptical about the future of this runtime.

This, however, didn't stop me from getting my hands on the project. This is what I got.

Installing deno

If you go to their website, you'll get the simplest indications ever for downloading and installing the runtime. And I mean it: it's way simple.

For any Unix-based (including any Linux distro) system, run the following:

curl -fsSL https://deno.land/x/install/install.sh | sh

In the case of macOS, it's pretty simple:

brew install deno

Finally, for Windows:

choco install deno

First Run

The immediately following section in the landing is Getting Started. There, they'll introduce two examples: the classic console.log("Hello World") and the HTTP-based Hello World.

You can find the console.log approach, running the following console statement:

deno run https://deno.land/std/examples/welcome.ts
A hello world for the masses

Then, there's the HTTP example.

import { serve } from "https://deno.land/std@0.53.0/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" });
}
server.js

I ran deno run server.js. And it failed.

One of the newest features, and most announced of deno is that the runtime is sandboxed. That means you MUST explicitly enable the permissions via flags.

Once you add the flags, it runs correctly. Go to https://localhost:8000/ and you'll see the magic.

Batteries included, but…

Then, I asked myself: what if I output a JSON with a 🦕 emoji? With a small modification to the initial snippet, we end up with something like this.

import { serve } from 'https://deno.land/std@0.53.0/http/server.ts';

const s = serve({
    port: 8000
});
console.log('http://localhost:8000/');

for await (const req of s) {
    req.respond({
        body: JSON.stringify({
            hello: '🦕'
        })
    });
}
server.js

However, when running in browser… it doesn't really look fine. Really, you'll get something similar to this:

{
    "hello": "🦕"
}

And what's that, anyways? Well… it turns out it's the ASCII-encoded version of the emoji (😅😅).

To fix this, the response must indicate it's encoded in UTF-8. How do you get this? Just add the corresponding header.

import { serve } from 'https://deno.land/std@0.53.0/http/server.ts';

const s = serve({
    port: 8000
});
console.log('http://localhost:8000/');

for await (const req of s) {
    req.respond({
        headers: new Headers({
            'Content-Type': 'application/json; charset=utf-8'
        }),
        body: JSON.stringify({
            hello: '🦕'
        })
    });
}
server.js: After adding Headers

Much fucking better ❤️🦕.

What about Routing?

How debugging looks on Visual Studio Code

One thing I noticed while debugging —notes on that later— this script is that, if I run the request on the browser, I'll get two requests in the server. The first request is for GET /. The second one is for GET /favicon.ico. This led me to plan a router with a 404 express-like fallback.

What's a Router? Basically, it's a code strategy that finds a handler given a request criteria. In this case, we route to method and uri.

How do we achieve that? We create an object that stores the handler functions for every method/uri, with a fallback if no method is found.

async handler (request) {
        const fallback = () => request.respond({
            status: 404,
            body: `Cannot ${request.method} ${request.url}`
        });

        const doRequest = 
              this.#requestsMap[request.method.toLowerCase()]?.[request.url]
                  ?? fallback;
        await doRequest(request);
}
MiniServer#handler: Notice the use of ESNext features like private fields, optional chaining and nullish coalescing

Then, replace the request.respond inside the for loop with a call to #handler.

async handleRequests () {
    for await (const request of this.#httpServer) {
	    await this.handler(request);
    }
}

Debugging

I wanted to check if the debugging worked the way it works with node. And indeed it did. However, there are slight changes:

  1. You have to specify the host:port inside the --inspect while on node, you only have to indicate the port, and the binding host is optional.
  2. If integrating with the Visual Studio Code debugger, you must add [ "run", "--allow-net" ] to the runtimeArguments. Also, you must set a fixed debugging port, since you also need to indicate the host, and VSCode doesn't do that by default.
{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
	{
      "type": "node",
      "request": "launch",
      "name": "Launch Deno Server",
      "runtimeExecutable": "deno",
      "runtimeArgs": [ "run", "--inspect-brk=0.0.0.0:9229", "--allow-net" ],
      "port": 9229,
      "program": "${workspaceFolder}/server.js",
      "skipFiles": [
        "<node_internals>/**/*.js"
      ]
    }
  ]
}
.vscode/launch.json modified to run deno

Wrapping up

The steps above mentioned are wrapped-up in the MiniServer: A Minimal HTTP Server for Deno library I just launched on GitHub. You are welcome to f0rk and improve it.

The usage for this library is pretty simple: just initialize the MiniServer and add the handlers.

import { MiniServer } from 'https://raw.githubusercontent.com/pandres95/deno-http-server/master/server.js';

const server = new MiniServer({
    port: 8000
});

server
    .get('/', async request => {
        request.json({
            body: {
                hello: 'deno 🦕'
            }
        });
    })
    .listen();