How Deno Improves on Node.js in the CI/CD Process

I am a big fan of Node.js. These days it seems as if it is everywhere, from standalone programs to serverless functions out on Google Cloud, AWS and Azure.

How Deno Improves on Node.js in the CI/CD Process

There’s a lot to like about the framework. JavaScript is the underlying language, so there’s a rich legacy of books, examples and tutorials that make it easy to get up to speed in terms of productivity. The loose-type nature of JavaScript promotes a flexible programming experience. And Node.js uses an event-driven loop architecture for implementing concurrency in a single-threaded environment. The event loop takes some getting used to; you need to get the hang of working with asynchronous code using callbacks and promises to make concurrency work. But in the grand scheme of things, it’s a small investment of time when compared to the benefits you get.

The bottom line is that companies that support Node.js development can realize a lot of productivity very quickly. However, Node.js does have its drawbacks. In fact, Ryan Dahl, the creator of Node.js, listed them in exacting detail in a presentation he did in 2018 at JSConf EU.

One drawback is the way Node.js stores third-party modules.

The problem with node_modules

You can think of a module as a library. The Node.js term used for a module is “package.” You can write your own packages or use those created by others. Developers download packages created by others from the central package repository, npm, or Node package manager. It’s possible to create private package repositories, but most developers use npm, which by the way was bought by Microsoft in March.

When developers want to use a third-party package on npm, they make an entry into the Node.js project’s package.json file. The entry defines the package name and version. Listing 1 below shows an example of a package.json file that uses two packages, connect and serve-static.

{
  "name": "simpleserver",
  "version": "1.0.0",
  "description": "A simple node server that published static HTML pages",
  "main": "index.js",
  "scripts": {
    "test": "test"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/reselbob/simpleserver.git"
  },
  "author": "Bob Reselman <reselbob@gmail.com>",
  "license": "MIT",
  "bugs": {
      "url": "https://github.com/reselbob/simpleserver/issues"
  },
  "homepage": "https://github.com/reselbob/simpleserver#readme",
  "dependencies": {
    "connect": "^3.7.0",
    "serve-static": "^1.14.1"
  }
}

Listing 1: A Node.js package.json file that uses two third-party packages, connect and serve-static.

To download the actual code for the two packages listed in package.json above, the developer invokes the command:

npm install

Whereas npm is the command-line executable for working with Node.js packages, and install is the subcommand that retrieves the packages listed in package.json from the npm website and installs them into the application’s file system.

The npm install command looks at the dependencies section in the application’s package.json file and downloads the packages listed. The third-party packages are downloaded and stored in a folder named node_modules.

File Tree!

Figure 1: Every Node.js project stores packages in its own node_modules folder.

Now here’s where things get a bit shocking. You’ll notice that only two packages, connect and serve-static, are listed in the dependencies section of the packages.json file shown in listing 1 above. Yet many more packages are downloaded into node_modules.

Why?

All the packages that connect and static-serve use are downloaded too. That’s a lot of unanticipated code. The result is that deploying multiple Node.js applications on demand in a CI/CD process incurs some noticeable overhead. Each application will have its own node_modules folder, yet many of the packages in a given node_modules folder might be redundant instances that appear in other instances of node_modules folders.

In other words, one package might exist in many applications. It’s the opposite of a fundamental principle of programming: don’t repeat yourself DRY.

Deno addresses the shortcomings of Node.js

The node_modules issue is but one of the many shortcomings of Node.js, and Dahl, the creator of Node.js, admits to them all. He also admits that things are so far along in the Node.js ecosystem that it’s just too late to offer a comprehensive fix.

He’s also done something about it: He created a new framework, Deno.

Deno still runs server-side on the JS Engine, just like Node.js. But then Deno veers into new territory.

First, the base language for Deno is TypeScript. The TypeScript compiler is built into the Deno runtime. Underneath the covers you still have JavaScript, but it’s been abstracted away.

The Deno security model is also tighter than the one Node.js uses. Node.js lets you use a lot of system resources without restriction. Deno is more strict.

In terms of dependency and library management, Deno takes a completely different route. package.json is gone. Rather, you declare objects directly with a source URL inline in code, as shown below in the first line of listing 2.

import { Application } from 'https://deno.land/x/oak/mod.ts'
import router from './routes.ts'

let app;

const HOST = '0.0.0.0'
const PORT = Deno.env.get("CALCULATOR_PORT") || 7700;

app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

console.log(`Listening on ${HOST}:${PORT} ...`);
await app.listen(`${HOST}:${PORT}`);

Listing 2: Deno declares the source of a library directly within the source code

Then, when you spin up the application, Deno has the smarts to go into all the source code files and download the dependencies according to the URL defined. And, instead of storing the library files in a directory distinct to the application — the way Node.js does with node_modules — under Deno, all the library files for all the applications are stored in a global cache location on the server. A developer invokes deno info on the machine where Deno is running to discover the location where dependency files are stored, as shown in figure 2 below.

Figure 2: Deno uses a global cache to store the dependency files for all applications

Deno improves CI/CD deployment performance

When it comes to deployment, Deno’s approach to dependency management has an impact on the CI/CD process.

First, it replaces Node.js’s “one node_module folder for each application” approach to library management with a common cache. This saves disk space. Remember, in some cases, a node_modules folder can contain a few hundred package subfolders. Hundreds of redundant folders can eat up valuable storage space on the disk when capacity allocations are very lean, such as in containers.

And then there’s the simple matter of network I/O. Fewer trips to the network to get dependencies results in faster application initialization. When you’re dealing with situations in which milliseconds count, eliminating redundant trips to the network makes a big difference.

Putting it all together

Deno is a development environment that shows a lot of promise. Using TypeScript as a base language enforces a programming discipline that goes with strongly typed, object-oriented languages. And its elegant approach to dependency management makes it attractive from a CI/CD and DevOps point of view.

Yet Deno is still a new kid on the block. Version upgrades to the standard library are frequent. Also, while the number of third-party modules available is still growing, the quantity pales when compared to the Node.js ecosystem. Still, these are not showstopper concerns.

No technology enjoys immediate widespread success on the first release. It takes time for things to catch on. As adoption rates continue to grow, we can expect to see Deno as a regular presence in a distributed application development environment.