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 🦕
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.
Useful links
- Official Deno examples
- Deno manual (includes information about Deno’s built in formatter and testing library)
- Deno standard library
- awesome-deno
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: