From Node.js to Deno

Introduction

In this article I've written down some of my thoughts on Deno, a new runtime for server-side typescript. Some time ago I wrote a kubernetes scheduler in Node.js for an internship project. After reading the latest article about deno on news.ycombinator.com I decided I wanted to try it out, since it seemed to solve most of the issues I have encountered with Node.js (more about that later on). Because I wanted to compare the two backend javascript runtimes, the best idea was to convert a recent node.js project into a deno project. The kubernetes scheduler was a perfect project for this conversion, since it had a small codebase with almost no external dependencies, and I could still clearly remember the development process.

Before we get into why exactly I chose to convert the codebase from Node.js to Deno, it might be a good idea to explain why this project was originally developed in Javascript with Node.js (well, actually, it was developed in typescript, but I'm trying to make a point here). Javascript, in my opinion, is a quite good programming language. I might even venture so far as to say that it is one of the best dynamic programming languages available at the time - surpassing even python. Javascript has the same ease of development as python; developing a feature would take roughly the same time in both languages. It is also significantly better at one critical aspect: code performance. A javascript program is often between one and two orders of magnitude faster than the equivalent python programs. On the other hand, it is true that javascript has more warts and peculiarities than python, but once you get to know the language you don't really encounter those anymore (or at least you know to avoid them).

The logical choice then (for a dynamic server-side programming language) would be Javascript/Node.js, were it not for a few significant issues:

1) Small standard library. There is only a very small standard library included with Node.js. It includes modules to do only the most essential tasks, such as interactions with the file system and the operating system. This has lead to a huge number of third-party modules being created for things that, in my opinion, should've been included in the standard library. A lot of these modules are also very compact, sometimes containing just a few lines of moderately complicated code. That, in turn, has caused a dependency explosion: every third-party module you install depends on dozens (often even hundreds) of other small dependencies. This is of course how it is supposed to work (also in other languages), but the sheer number of indirect dependencies that a node.js module might import is a huge security risk. Bad actors can take over a module (as has happened several times before) and deploy malicious code that is then automatically picked up by your continuous integration system.

2) No types. Types are an absolute necessity when working on large codebases. Javascript is very pleasant to work with on smaller codebases where you can keep all the code in your head. However, once you start working on larger, more professional codebases that contain more than hundreds of thousands lines of code, the lack of types becomes an acute issue. Most of the code will not be written by you, and the code that is written by you will seem foreign in a matter of months. It is therefore of great importance to have contracts defining the inputs and outputs of a function - i.e. a type system - so you don't have to do a deep dive into the source code for every moderately complex function that you want to use. Python also had this issue, however that seems to have been solved by the recently added type hints feature.

The issues described above can all be solved: for the types we simply need to use typescript, which transpiles down to Javascript amd provides compile-time type checking. This solves the issues where you do not know the interface of a function or class in a large codebase. To solve the issues with the small standard library (and the messy configuration for setting up typescript), we have to look towards Deno.

Why Deno

Deno works out-of-the-box with Typescript, meaning that no transpiling has to be done as is the case with Node.js. Many other things such as debugging also work without the need for any kind of setup. This alone is already a major selling point for Deno in my opinion.

Another big improvement over Node.js is the standard library, as mentioned a few paragraphs back. While it would be unfair to say that node has no standard library (it has some utilities to interact with the operating system and to replace browser APIs), it definitely leaves quite some room for improvement. Take for example the python standard library; it contains pretty much anything that a developer will need for a reasonably sized application. Python's standard library enables a developer to work on actual features, without having to worry about basic stuff such as command line argument parsing or unit testing. It also allows for safer applications, because there's less third-party packages that can become abandoned, break or contain malicious code. That is exactly what a standard library should aim for.

Finally, Deno also comes with batteries included. It comes with linting, a language server, application bundling, unit testing and formatting all built in. This makes for a very smooth development experience. You can just get started with a Deno project, no configuration needed (reasonable defaults are already provided).

The conversion

While deno itself works without issues, the surrounding ecosystem is not completely ready yet for prime time. There is no official docker image yet, for example. Building your own deno base image is rather simple however:

FROM debian:10-slim

# Install deno
RUN apt update -y \
    && apt install -y --no-install-recommends curl ca-certificates unzip \
    && curl -fsSL https://deno.land/x/install/install.sh | sh \
    && mv "$HOME/.deno/bin/deno" /usr/bin/deno \
    && apt remove -y curl ca-certificates unzip \
    && apt autoremove -y \
    && rm -rf /var/lib/apt/lists/*

The end result is an image of about 150 MB.

Setting up the development environment is a smooth process. The installation of deno itself is quite easy, you basically just copy a bash command from the deno website and the install script does everything else. On linux there are still some manual operations needed, which are explained by the install script though. The extension for the VSCode editor works adequately, but it still has some rough edges here and there. I still had to set the path for the deno executable myself, and sometimes the editor wouldn't properly propagate typing changes to the language server.

Deno itself works like a dream, as mentioned before. There's no need for any setup, you can just start developing. The deno workflow is basically: 1) create an index.ts file, 2) fill it with some typescript code and 3) run it with deno run index.ts.

Most of the typescript code from the node project could just be copy-pasted without issues. I even managed to remove some dependencies, such as the logger (which was already included in the deno standard library). On the other hand, for some external dependencies I did have to write my own replacements as I could not find any good third-party packages. In the old project I used convict (a configuration library), and a kubernetes API library, both which had no good replacements. Writing the drop-in replacements for those libraries took some extra time, which wasn't an issue originally while developing the node project.

One significant difference between node and deno is that deno has implemented a coarse permission system. In order to allow a deno program to access a file or connect to a website you need to specify permissions in the deno run command. I didn't have much trouble with this system, since I only encountered the file system permission and the network permission. It seems like a good system from the limited experience I had with it, but I will refrain from any more opinions for the time being until I've learned more.

Conclusion

In the end, I've come to the following conclusions regarding deno, and when you should use it. Deno, as it is right now, is very useful for small greenfield projects that don't use much (if any) dependencies. The standard library and the third-party modules have not matured enough to use it for larger projects. For those kinds of projects one should still use Node with Typescript, as the benefits still outweight the security risks and the lack of standard library.

However, in a year or two, once Deno has done some maturing and there are more stable third-party packages available, then it might be a good idea to give Deno a good look again when starting a new large software project (where previously you would've used Node).

Small sidenote: Converting old node projects to Deno might not be worth it even then, since the third-party modules cannot just be ported from one runtime to the other. Also, you will have to switch all Node-specific code to Deno-specific code.