Categories


Archives


Recent Posts


Categories


Comparing a Deno and Node.js Hello World Program

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 2 of 2 in the series Exploring Deno. Earlier posts include Where does Deno Code Come From. This is the most recent post in the series.

In the world of web services your standard issue hello world program will

  1. Start a webserver
  2. 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.

Series Navigation<< Where does Deno Code Come From

Copyright © Alan Storm 1975 – 2020 All Rights Reserved

Originally Posted: 10th August 2020