Want a free comprehensive digital product evaluation? Find out more

An Introduction to Deno

By

Since its introduction in 2009, Node.js has gained huge popularity and usage. But with that, issues with its ecosystem, feature adoption and dependency bloat have started to surface.

So, in true JavaScript community style, there's a new kid on the block: Deno 🦕

Image

What is Deno?

Deno is a new runtime for JavaScript and Typescript, built on Google's V8 engine and written in Rust. It was started by Ryan Dahl (who famously started Node.js) as an answer to the problems he saw with Node.js and its ecosystem.

Ryan announced the project a couple of years ago at JSConf EU during a talk in which he went into some detail about regrets he had over Node.js, particularly around decisions he did (or didn't) make along the way. It's definitely worth a watch.

Although seen as a Node.js successor, there are some major differences between the two:

  • Deno has no package manager.
  • Deno implements a security sandbox via permissions.
  • Deno has a standard library for common tasks.
  • Deno has first-class TypeScript support.
  • Deno will be able to be compiled into a single executable.

No package manager

Instead of the complex module resolution that Node.js supports, Deno simply uses URLs for dependencies and doesn't support package.json. Import a relative or absolute URL into your project, and it'll be cached for future runs:

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

Third party modules can be added to Deno's website via https://deno.land/x/.

Security

By default, a Deno application will not be able to access things like your network, environment or file system. Unlike Node.js, in order to give an application access to this sandboxed functionality you'll need to use one of the provided flags:

$ deno run server.ts --allow-write

You can see all of Deno's supported security flags by running deno run --help.

Standard library

Much like Go, the Deno team maintains a core, stable set of utilities in the form of a standard library. These cover utilities such as logging, http serving and more. If you need to implement a feature, it's probably best to check the standard library first to see if it's already supported.

You can see what's available in Deno's standard library via its source code.

TypeScript

Unlike Node.js, Deno has first-class support for TypeScript (most of its standard library is written in it). This means that ES modules and all the goodness of static typing are available right from the start, with no transpilation required on the user side. It's worth noting however that Deno still needs to compile TypeScript to JavaScript behind the scenes, and as such incurs a performance hit at compile time unless the module's already been compiled and cached.

If you'd rather not use TypeScript, Deno supports JavaScript files too.

Single executables

Although not implemented yet, one future ambition is to allow a Deno application to be compiled down into a single executable. This could vastly improve and simplify the distribution of JavaScript-based applications and their dependencies.

You can track the progress of single executable compilation on GitHub.

Running Deno

Now we know what Deno is, let's have a play with it.

The Deno website provides plenty of installation options, but since I'm using macOS I'll use Homebrew:

$ brew install deno

Once installed, deno should be available to use from your terminal. Run deno --help to verify the installation and see what commands it provides.

Deno also gives the ability to run applications with just a single source URL. Try running the following:

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

Download https://deno.land/std/examples/welcome.ts
Warning Implicitly using master branch https://deno.land/std/examples/welcome.ts
Compile https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕

Deno downloads the module from the provided URL, compiles it and runs the application. If you visit the above module's URL in your browser, you'll notice that Deno also provides a nice browser UI for the module's source code, which in this case is a simple console.log statement.

Of course running arbitrary third party code like this should always be treated with caution, but since it's an official Deno example we're all good here, and as mentioned above, Deno's security flags should help limit any potential damage.

You'll also notice that if you run the same command again, the welcome.ts module isn't redownloaded. This is because Deno caches modules when they're first requested, allowing you to continue work on your project in places with limited internet access.

If for any reason you want to reload any of your imports, you can force this by using the --reload flag:

$ deno run --reload https://deno.land/std/examples/welcome.ts

Building your first Deno app

To demonstrate a few of Deno's features, let's dive into a simple API example. Nothing too complicated, just a couple of endpoints. And in true Potato style, we'll use different types of spuds for our test data.

It's worth noting beforehand that this demo won't rely on any third party modules, and will use an in-memory data store. There are plenty of libraries (some are linked at the bottom of this article) that aim to make this simpler, but for now let's stick with vanilla Deno!

Setting up the server

Firstly, let's create a TypeScript file. Don't worry too much if you're not familiar with TypeScript, you can use plain JavaScript too. I'll create mine at server.ts.

Next, we need to set up a simple web server. As we've already seen, Deno has a standard library that contains some useful functions with one of these being the http module. Taking inspiration from Go, there's a helpful listenAndServe function that we can use:

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

listenAndServe({ port: 8080 }, async (req: ServerRequest) => {
  req.respond({ status: 204 });
});

console.log("Listening on port 8080.");

What's happening here? Firstly, we import the listenAndServe method from Deno's http module, and the ServerRequest interface to allow TypeScript type checking. Then, we create a simple server that listens on port 8080 and responds to all requests with a HTTP 204 No Content response.

As mentioned above, by default Deno will prevent our application from accessing the network. To run this successfully, we'll need to use Deno's --allow-net flag:

$ deno run --allow-net server.ts

We can verify our application is running correctly using cURL in another terminal tab:

$ curl -i -X GET http://localhost:8080

HTTP/1.1 204 No Content
content-length: 0

Environment variables

To show how environment variables are passed to Deno, let's add support for a dynamic port number since this is a common use case amongst production servers. Deno provides the Deno.env runtime library to help with retrieving the current environment variables:

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

const { PORT = "8080" } = Deno.env.toObject();

listenAndServe({ port: parseInt(PORT, 10) }, async (req: ServerRequest) => {
  req.respond({ status: 204 });
});

console.log(`Listening on port ${PORT}.`);

We can now pass a custom port to our application when running it. One thing to note here is that we need to convert the port variable to a number, since all environment variables are passed as strings and listenAndServe expects a number for the port.

When running this, we'll also need to use the --allow-env flag to grant the application access to our environment variables:

$ PORT=6060 deno run --allow-net --allow-env server.ts

Routes

For the sake of simplicity, we'll implement a very simple router ourselves using a good old fashioned switch statement.

Firstly, let's create some empty route handlers. We'll create two: one to allow a new spud type to be added to a list, and another for retrieving the current list. For now, let's return a HTTP 204 No Content response so that we can test our application along the way:

const createSpud = async (req: ServerRequest) => {
  req.respond({ status: 204 });
};

const getSpuds = (req: ServerRequest) => {
  req.respond({ status: 204 });
};

Next, let's create a handleRoutes method that'll act as our router:

const handleRoutes = (req: ServerRequest) => {
  if (req.url === "/spuds") {
    switch (req.method) {
      case "POST":
        createSpud(req);
        return;
      case "GET":
        getSpuds(req);
        return;
    }
  }

  req.respond({ status: 404 });
};

Here, we're checking every incoming request URL and method, and directing the request to the appropriate function. If neither the URL nor the method matches anything expected, we return a HTTP 404 Not Found to the user.

Finally, let's call the handleRoutes function from our original server and add a try statement around it to catch any errors and return an appropriate response:

listenAndServe({ port: parseInt(PORT, 10) }, async (req: ServerRequest) => {
  try {
    handleRoutes(req);
  } catch (error) {
    console.log(error);
    req.respond({ status: 500 });
  }
});

Using a try statement and catching errors in this way is usually a good idea with Deno, since unlike Node.js a Deno application will exit when it encounters an uncaught error.

We should now be able to send POST and GET requests to http://localhost:8080/spuds and get an expected HTTP response:

$ curl -i -X GET http://localhost:8080

HTTP/1.1 404 Not Found
content-length: 0

$ curl -i -X GET http://localhost:8080/spuds

HTTP/1.1 204 No Content
content-length: 0

$ curl -i -X POST http://localhost:8080/spuds

HTTP/1.1 204 No Content
content-length: 0

Create handler

Next, let's add an in-memory store for our spud types:

const spuds: Array<string> = [];

In order to process the incoming spud data, we'll need to be able to parse the request's JSON body. Deno doesn't have a built in way of doing this at the time of writing, so we'll use its TextDecoder class and parse the JSON ourselves:

const createSpud = async (req: ServerRequest) => {
  const decoder = new TextDecoder();
  const bodyContents = await Deno.readAll(req.body);
  const body = JSON.parse(decoder.decode(bodyContents));
};

What's happening here? Essentially, we're first using the Deno.readAll method to asynchronously read the contents of the request body (a Reader) as bytes. We then decode that into a UTF-8 string, and finally parse it as JSON. Phew.

We can then proceed to add the spud type to the store we created earlier, and return a HTTP 201 Created response. Our final create handler should look something like this:

const createSpud = async (req: ServerRequest) => {
  const decoder = new TextDecoder();
  const bodyContents = await Deno.readAll(req.body);
  const body = JSON.parse(decoder.decode(bodyContents));

  spuds.push(body.type);

  req.respond({
    status: 201,
  });
};

Get handler

To implement our GET handler, we'll essentially reverse the operation we wrote above by using Deno's TextEncoder. We'll then set the relevant header to "application/json" using Deno's Headers class and return the spud data with a HTTP 200 OK response:

const getSpuds = (req: ServerRequest) => {
  const encoder = new TextEncoder();
  const body = encoder.encode(JSON.stringify({ spuds }));

  req.respond({
    body,
    headers: new Headers({
      "content-type": "application/json",
    }),
    status: 200,
  });
};

Final application

Our final file should look a bit like this:

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

const { PORT = "8080" } = Deno.env.toObject();

const spuds: Array<string> = [];

const createSpud = async (req: ServerRequest) => {
  const decoder = new TextDecoder();
  const bodyContents = await Deno.readAll(req.body);
  const body = JSON.parse(decoder.decode(bodyContents));

  spuds.push(body.type);

  req.respond({
    status: 201,
  });
};

const getSpuds = (req: ServerRequest) => {
  const encoder = new TextEncoder();
  const body = encoder.encode(JSON.stringify({ spuds }));

  req.respond({
    body,
    headers: new Headers({
      "content-type": "application/json",
    }),
    status: 200,
  });
};

const handleRoutes = (req: ServerRequest) => {
  if (req.url === "/spuds") {
    switch (req.method) {
      case "POST":
        createSpud(req);
        return;
      case "GET":
        getSpuds(req);
        return;
    }
  }

  req.respond({ status: 404 });
};

listenAndServe({ port: parseInt(PORT, 10) }, async (req: ServerRequest) => {
  try {
    handleRoutes(req);
  } catch (error) {
    console.log(error);
    req.respond({ status: 500 });
  }
});

console.log(`Listening on port ${PORT}.`);

Let’s give this a test:

$ curl -i --data '{"type": "maris piper"}' -X POST http://localhost:8080/spuds            

HTTP/1.1 201 Created
content-length: 0

$ curl -i --data '{"type": "king edward"}' -X POST http://localhost:8080/spuds            

HTTP/1.1 201 Created
content-length: 0

$ curl -i -X GET http://localhost:8080/spuds                            

HTTP/1.1 200 OK
content-length: 54
content-type: application/json
{"spuds":["maris piper", "king edward"]}

If you'd rather, you can view this file as a Gist or run it directly with the following command:

$ deno run --allow-net --allow-env https://gist.githubusercontent.com/dcgauld/205218530e8befe4dfc20ade54e7cc84/raw/9eff7733cf017f33b2bf3144937f97702ae4fc63/server.ts

We just created our first Deno application!

Conclusion

Hopefully this article has given you a glimpse into the world of Deno, and some inspiration to start using it for future projects. I'm excited to see what the future holds for the project, especially around things like single file executables and the potential to run certain Deno modules in the browser.

If you'd like to learn more about it and its features, I'd really recommend giving the Deno manual a read.

We created our first Deno API with no third party modules, but there are many libraries out there already that aim to simplify that process. Some examples: