- Where does Deno Code Come From
- Comparing a Deno and Node.js Hello World Program
In the world of web services your standard issue hello world program will
- Start a webserver
- Have that web server respond to a request with the text Hello World
Comparing Deno’s current hello world program with a similar example in Node.js can tell us a lot about the differences in this new javascript runtime.
This article was written in the summer of 2020 using Deno 1.2.2. If you’re coming to this article from the future — some specifics might have changed, but the concepts should remain solid. Also — I’m just some rano on the internet figuring this all out. If something seems wrong here it probably is — let me know and I’ll work to get it fixed up.
The Programs
Let’s take a look at the two programs.
Node.js
In Node.js, you can setup a simple web server using the following program
// File: node.js
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end("Hello World\n");
});
server.listen(3000, '127.0.0.1', () => {
console.log(`Server running at http://127.0.0.1:3000/`);
});
Run this program
$ node node.js
and then in a separate terminal window (or in your browser of choice), make a request to your new hello world service.
$ curl http://127.0.0.1:3000
Hello World
This program is a modified version of the hello world program from the official Node.js guides.
Deno
Similarly, in Deno, you can setup a web server using the following program
// File: deno.js
import { serve } from "https://deno.land/std/http/server.ts";
const server = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of server) {
req.respond({ body: "Hello World\n" });
}
Run this program
$ deno run --allow-net deno.js
and then in a separate terminal window (or in your browser of choice), make a request to your Deno hello world service.
$ curl http://localhost:8000
Hello World
Invocation Differences
The first difference we’ll want to look at is the invocation of our programs.
$ node node.js
$ deno run --allow-net deno.js
Deno’s invocation is a little more complex than Node’s. First, Deno requires a sub-command — run
— to run the program. Second, Deno requires us to use the --allow-net
flag.
Deno’s Sub-Commands
The run
sub-command is a side effect that falls out of Deno’s “no built in package manager or npm
like program” philosophy. In the Node.js ecosystem, npm
serves as a package manager, but it’s also the foundation of a programmer’s development enviornment. You end up using npm
scripts to run things like linters, a project’s tests, etc.
Instead of relying on a separate tool to do this, Deno has a number of built in sub-commands. One of these, run
, will run our program — but there are others. For example, instead of using a third party linter, you can check the format of your program(s) with the fmt
command
$ deno fmt deno.ts
Or you can run a project’s test modules with
$ deno test src
Deno’s Security Flags
The --allow-net
flag is an example of Deno’s security-by-default philosophy. Deno asks that the human running a program give that program permission to do certain things that might be “sensitive security areas”. If you try running the program without this flag, you’ll get an error
error: Uncaught PermissionDenied: network access to "0.0.0.0:8000",
run again with the --allow-net flag
Because our program starts a web server, we need to give our program permission to use the network. i.e. we run it with the --allow-net
flag.
CommonJS vs. ECMAScript Modules
The next difference to look at is how modules are imported.
const http = require('http');
import { serve } from "https://deno.land/std/http/server.ts";
In NodeJS, we use the require
function to pull in code from other modules. NodeJS modules are, by and large, written in the CommonJS module format and require
is how you pull in CommonJS modules.
In Deno, we use the import
statement to pull in code from other modules. Deno’s modules are, so far, all distributed in ECMAScript Module format.
Node.js recently introduced support for ECMAScript modules — but the bulk of existing NodeJS code is written using the CommonJS format, and getting projects working that use both module formats is — tricky. Also, bundlers and compiler projects like Webpack and Babel have long allowed engineers to use import
in any javascript program, as long as they’re comfortable with the final program being compiled javascript.
Deno tries to leave all this behind and officially supports only the ECMAScript modules.
Another difference with module loading in Deno is where you can load modules from. Deno can load modules from your local file system (not seen above) or from the internet. Our Deno program is literally loading code from the deno.land
URL — you can see this using curl
# The `Accept: text/plain` is neccesary because the `http://deno.land`
# server gives browsers an HTML version of the page instead of the raw
# code
$ curl -H "Accept: text/plain" https://deno.land/std/http/server.ts
/* ... module output snipped ... */
In NodeJS you might be loading a module from a relative file path, or you might be loading it from the node_modules
folder, or it might be a module that’s a built-in part of the NodeJS runtime.
Which is a nice segue to our next difference.
Runtime vs. Standard Library
In NodeJS, the http
module
const http = require('http')
is part of the Node.js “runtime”. That is — there’s no http.js
file distributed with Node.js — instead the http
module is just something built-in to the Node.js binary. If you’re familiar with the Node.js implementation you know that these built-in runtime modules are implemented in javascript — but it’s special javascript with mechanisms for calling into the C++ code at the heart of Node.js (if you actually know what’s going on in Node.js’s source — my apologies for the gross over simplification).
For example, the source for the http
module can be found here.
Deno takes a slightly different approach. Deno has a lightweight built-in runtime of around 130 methods, functions and classes.
Everything else in Deno is the standard (std) library, which is code written in TypeScript that uses these runtime functions. So when we import the server module
https://deno.land/std/http/server.ts
there will be a compiled copy of server.ts
on our computer (in our DENO_DIR
). There’s no complex binding into a lower level language in this file either — server.ts
just uses the Deno runtime functions. (Deno.listen
specifically)
While writing code for the Deno runtime still requires deep traditional systems level programming knowledge (but with Rust!) — all you need to know to contribute to the standard library is TypeScript and knowledge of the methods exposed by the Deno runtime.
The HTTP Server APIs
We’re finally at the part where we talk about the actual hello world program itself.
If we look at the code that handles the incoming HTTP requests.
// Node.js
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end("Hello World\n");
});
// Deno
for await (const req of server) {
req.respond({ body: "Hello World\n" });
}
The first difference that might pop out is that in the Node.js code we’re dealing with a separate request object (req
) and response object (res
), while in Deno we have a single request object (req
). In Deno, when we want to respond, we call this object’s respond
method. It’s a small difference, but Deno does change the semantics of how you respond to a request.
The bigger change is Deno’s dropping of callback APIs.
In the Node.js code, the createServer
method accepts an anonymous javascript function (i.e. a callback) that Node will call whenever it receives a request. Using anonymous functions as asynchronous callbacks was the bread and butter of the original Node.js APIs. In some ways Node.js was an experiment in how well this sort of callback programming would work.
While callback programming is possible in Deno — the official APIs avoid it in favor of Promises, or, in the case of the HTTP server — an asynchronous iterator. The server
object above will yield
whenever the server’s underlying listener receives a request. If you’re not sure what an async iterator is you might be interested in the Four Steps to Async Iterators quickies series, where we walk you though everything you need to know in order to understand javascript’s async iterators.
While Promises and async iterators aren’t new, their adoption in Node.js packages and core functionality has been slow. Deno’s hard break with these callback APIs points to, perhaps, the biggest difference between Deno and Node.js. Node.js is a mature project, run by a foundation, and it’s looking towards its own status quo. Deno — while immature and unproven, is looking towards a new and different future.