Deno – the end of Node.js?

Will Deno make everything better and replace Node.js? Node.js has long been an established environment for the execution of server-side JavaScript. Modern IT landscapes would be unthinkable without it; companies like Uber or Netflix have been using Node.js for years. But with the release of Deno, a server-side execution environment for TypeScript and JavaScript, this could soon change. Outdated traditions are ended and modern concepts are introduced. Will Deno join the IT landscape or even push the veteran Node.js from its place?

In mid-2019, Ryan Dahl spoke at JSConf EU in his talk “10 Things I Regret About Node.js” about the things he could have done better at Node.js. Dahl is the founder and initial lead developer of the server-side JavaScript platform Node.js. He spoke very openly about the fact that, for example, the resolution of the modules via node_modules is too complex, that the package.json file was born out of necessity, and that he is responsible for the fact that Node.js is callback-based and not promise-based. At one point, Node.js even implemented promises, but Ryan removed that again because he didn’t believe in promises at that time. Meanwhile, promises are established as a standard tool. Furthermore, he says, Node.js has never had a sensible security concept, even though the V8 engine used for JavaScript interpretation has everything on board. He simply did not implement it.

It’s time to make it all better! At the conference, Dahl presented his latest development. Similar to his first Node.js presentation it is a very young prototype: Deno – a secure runtime environment for JavaScript and TypeScript [1]. Like Node.js, Deno is based on V8 and, unlike Node.js, uses Rust instead of C++ for the development of Deno itself. Since Ryan is a big TypeScript lover, it is natural that Deno supports TypeScript. Node.js itself can be used with TypeScript, but you have to implement the compiling and debugging support yourself. With Deno, everything comes out of the box. About one year later, in May 2020, Deno was officially released in version 1.0. This was made possible not only by Ryan himself but also by the 328 contributors.

It is important to know that Deno has made a conscious decision to do a little spring cleaning and not be compatible with Node.js. This decision enables Deno to cut off outdated practices and work with the most modern development methods and concepts possible. Deno sees itself as a web browser for executing command-line scripts, so many browser APIs should work as usual in Deno, e.g. fetch.
It is time to take a closer look at Deno and see if it could be a Node.js killer.

First steps with Deno

In this article, we will explore Deno using a small example. For this purpose, we implement a small HTTP API with access to a SQLite 3 database to manage a simple list of computer games. The finished example can be found on GitHub [2].

Deno must be installed first. On the installation page [3] there are different possibilities for all operating systems. A variant that allows multiple versions is recommended, for example with the runtime manager asdf [4], since Deno currently plans on releasing a new version every two weeks.

Both popular IDEs, VS Code and JetBrain’s WebStorm, support development with Deno. For VS Code the official plugin [5] needs to be installed. For WebStorm the Deno support can be enabled in the preferences of the IDE. However, both offer very early support of Deno, so expect that some things may not work as expected [6]. The overall support is still very rudimentary.Once everything is installed, create a project folder windows-developer-deno and load it into our IDE. Next, create a folder src and a file index.ts. In the index.ts we first write a log output:

console.log("Hello Deno!");

With the help of a command line we execute the file:

deno run index.ts

As a log output, we first receive the message that Deno is compiling our file. Even though Deno supports TypeScript as a first-class citizen, it still has to compile TypeScript to JavaScript, since the underlying V8 can only interpret JavaScript. After compilation, we should see the text “Hello Deno!”. The first step with Deno is made!

Development of the HTTP server

The core of our HTTP API is of course an HTTP server. For this purpose, we create the file src/http-server.ts. Listing 1 shows the contents of the file.

Listing 1: Content of the file src/http-server.ts

import { Application, Router } from "https://deno.land/x/[email protected]/mod.ts";
 
export class HttpServer {
  private readonly app = new Application();
 
  constructor(private readonly port: number) {}
 
  async listen(): Promise<void> {
    const router = new Router();
 
    this.app.use(router.routes());
    this.app.use(router.allowedMethods());
 
    console.log(`Running HTTP Server on port ${this.port}`);
    await this.app.listen({ port: this.port });
  }
}

In the first line of Listing 1 we see a direct difference to Node.js: We import application and router from one URL. As mentioned in the beginning, the package.json from Node.js was one thing Ryan Dahl regrets. Therefore this concept no longer exists in Deno. Instead, all modules are loaded via an absolute or relative URL.

Deno’s modular system

Deno currently offers two standard repositories for modules. https://deno.land/x/ describes third-party modules. Unlike npm, the modules are not uploaded to https://deno.land/x/. Instead, the service acts as a pure URL rewrite service and then forwards the request to the target, such as GitHub. Instead of https://deno.land/x/, you could also import directly from GitHub.

The second repository is https://deno.land/std/, a collection of standard modules that work without additional dependencies and have been reviewed by the Deno core team. This guarantees that these modules are of high quality and version X of them is always compatible with Deno in version X. Even though, as mentioned at the beginning, Deno is deliberately not compatible with Node.js, the first approach to make CommonJS modules from Node.js compatible with Deno already exists at https://deno.land/std/node. At the moment, however, this approach is still a proof-of-concept..

At runtime, before Deno compiles our TypeScript, all dependencies are downloaded and stored in a central cache (instead of in a node_modules folder in each project). This central cache is also never deleted by Deno. If you want to download modules again, you have to specify the –reload flag at startup. Typically, the modules versioning is done directly in the URL during import. See our example in Listing 1, where we import the module Oak in version 4.0.0, which corresponds to a branch or tag in the corresponding GitHub repository. If you were to omit the version, you would get the current master branch of the repository as a module. For the update, the flag –reload is then mandatory.

Oak is one of the first third-party modules to develop a middleware-based HTTP API. Oak’s API is inspired by the Node.js module Koa. If you prefer a web framework instead of a server module, take a look at the module Alosaur [7]. Similar to .NET Core, it brings features like dependency injection, decorators, controllers, view rendering, and SPA framework integration. In our article example, we use the simpler Oak, because we want to get to know Deno first and not a specific framework.

Bye callbacks, hello promises!

Further in Listing 1 we define the class HttpServer and create an instance of Application in a private field. Application is a class of the module Oak and offers us an HTTP server. Via the constructor, we get the port with which our server should be started later.

The listen method is used to finally start our HTTP server on the specified port. For this purpose we create an instance of the Oak-Router, which later allows us to map URLs to functions. We then use these routes and the corresponding HTTP methods as middleware in our application. Last but not least, we start the HTTP server with this.app.listen. Here we also see another difference. With Node.js all servers were usually equipped with a callback. Good news: Callbacks in Deno are history. Instead, Deno uses only promises for all asynchronous operations, meaning that we can develop our code using the async/await pattern.

To start our HTTP server, we first switch back to the file index.ts and replace the content with the one in Listing 2.

Listing 2: Content of src/index.ts

import { HttpServer } from "./http-server.ts";
 
const port = Deno.env.get("PORT") || 8080;
 
const httpServer = new HttpServer(+port);
 
await httpServer.listen();

In Listing 2 we first import our HttpServer via a relative URL. Then we read out the environment variable PORT via Deno.env and use 8080 as our default if the variable is not set. In general, all APIs that do not conform to the web standard are available under the global object Deno, for example: reading files, opening TCP sockets, and calling other processes.

After reading the port we create an instance of the HttpServer and call the method list. Exciting is the fact that we can already use await at the top level in the file without wrappers or similar.

Security concept: Do what you want, but I’ll tell you what you can do

If we start our program via deno run index.ts, first the dependencies are downloaded, our TypeScript is compiled, and then we are welcomed with an error message:

error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flag

As mentioned at the beginning, Deno sees itself more as a web browser. We are accustomed to the browser providing some security when a website wants to access certain APIs, such as the camera or microphone. This concept was transferred to Deno. Any code runs in a sandbox, without any further rights. Any access to resources such as operating system, environment variables, network or file system must be explicitly allowed when the application is started.

So for using environment variables we need the flag –allow-env, for the HTTP server we need –allow-net. Therefore we have to start our application with this command:

deno run --allow-env --allow-net index.ts

This command can also be stored in a shell script. After the execution, our application starts. In order for it to really be able to respond to HTTP requests, we need to add controllers and the ability to access a SQLite 3 database.

Database access with denodb

In order to access a SQLite 3 database, we will use the module denodb [8]. denodb is also one of several modules that already allow access to databases. We use SQLite 3 here to avoid the installation of a database engine. denodb itself can address MySQL or PostgreSQL databases in addition to SQLite 3.

First, we create a file src/database/game.entity.ts. The content of the file can be found in Listing 3.

Listing 3: Content of the file src/database/game.entity.ts

import { DATA_TYPES, Database, Model } from "https://deno.land/x/denodb/mod.ts";
 
export class GameEntity extends Model {
  static table = "games";
  static timestamp = true;
 
  static fields = {
    id: {
      primaryKey: true,
      autoIncrement: true,
    },
    name: DATA_TYPES.STRING,
    type: DATA_TYPES.STRING,
    publisher: DATA_TYPES.STRING,
    developer: DATA_TYPES.STRING,
  };
}

The API of denodb expects that one class of Model must be derived per database entity, here our GameEntity. The model is described using static fields. The field table indicates the table in which the entity is stored. The field timestamp specifies whether, in addition to our fields defined in fields, the fields created_at and updated_at are also created and updated when the data is manipulated. In the fields section, there is an object with the fields of the model. Each key is mapped to a column. For simple columns, the data type can be defined using DATA_TYPES. If a column (like id) needs further meta information, this is done via another object. It should be noted that denodb is currently not yet able to express relations. This feature is still being implemented.

The next step is to create the file src/database/index.ts. We find the content in Listing 4.

Listing 4: Content of the file src/database/index.ts

import { Database } from "https://deno.land/x/denodb/mod.ts";
import { GameEntity } from "./game.entity.ts";
 
export class DatabaseProvider {
  private connection?: Database;
 
  async connect(filepath: string): Promise<void> {
    console.log("Connecting to DB", filepath);
 
    this.connection = new Database("sqlite3", { filepath });
    this.connection.link([GameEntity]);
    await this.connection.sync({ drop: true });
  }
 
  async save(): Promise<void> {
    await this.connection?.close();
  }
}
 
export const databaseProvider = new DatabaseProvider();

In Listing 4 we see the implementation of the DatabaseProvider class. It provides two methods. The connect method connects to our SQLite 3 via the specified file path filepath. For this purpose, a new instance of the Database class is created. Via the method link, we communicate all models we want to link to the database. The method sync ensures that all tables and columns are created. With the drop setting we specify that all model tables in the database will be deleted and created again. This is very handy at development time, however, for production this feature should be switched off.

The second method saves our changes in the database. Here you can still find a small design problem (bug) from denodb. There is no Commit for SQLite 3 yet, and writing to the database does not take place until the connection is closed. Therefore, when we save, we simply close the connection to the database and denodb will re-establish it if necessary. Finally, we create an instance of the DatabaseProvider and export it as the variable databaseProvider.

To ensure that the connection is established once when the application is started, we add the code from Listing 5 to our src/index.ts file, immediately before the httpServer.listen location.

Listing 5: Addition in the file src/index.ts

const filepath = Deno.env.get("DB_FILEPATH") || "./windows-developer.sqlite";
await databaseProvider.connect(databaseConfiguration);
// httpServer.listen ...

In Listing 5 we load the path to the database via the environment variable DB_FILEPATH. If it is not set, we use windows-developer.sqlite as the default value. We then use the connect method to establish a connection to the database.

Connection to the outside world: HTTP Controller

One link between the database and the outside world is still missing: an HTTP controller that accepts HTTP requests and communicates with the database. Normally you would add an additional service layer so that the controller communicates with a service and the service communicates with the database entities. For the demo in this article, we do without this additional indirection and let the controller talk directly to the database.

Start by creating the file src/controllers/game.controller.ts. We find the content in Listing 6.

Listing 6: Content of the file src/controllers/game.controller.ts

import { Router, RouterContext } from "https://deno.land/x/[email protected]/mod.ts";
import { GameEntity } from "../database/game.entity.ts";
import { databaseProvider } from "../database/index.ts";
 
export class GameController {
  constructor(router: Router) {
    router.get("/games", (context) => this.list(context));
    router.post("/games", (context) => this.create(context));
    router.put("/games", (context) => this.update(context));
    router.delete("/games/:id", (context) => this.delete(context));
  }
 
  async list(context: RouterContext): Promise<void> {
    context.response.body = await GameEntity.all();
  }
 
  async update(context: RouterContext): Promise<void> {
    const { value } = await context.request.body();
    const { id } = value;
 
    const entity = await GameEntity.find(id);
 
    if (!entity.length) {
      return;
    }
 
    try {
      await GameEntity.where("id", id).update(value);
      await databaseProvider.save();
      context.response.status = 200;
    } catch (error) {
      console.error(error);
      context.throw(500);
    }
  }
 
  // Method create and delete, please see the final GitHub example
}
 

The GameController implements a CRUD interface for our GameEntity. For demonstration purposes, only the methods list and update are completely displayed in this article. The create and delete methods are identical in structure and can be viewed in the final example on GitHub [2].

In the constructor, we use Oak’s router and implement four HTTP routes to list all our entities, create, delete, and update an entity.

Let’s take a closer look at the two methods list and update. By definition, when using the Oak Router, a handler has a parameter context of the type RouterContext. On this object, there is information about the request and we can give details about the response.

In the method list, the response of the HTTP request is quite simple. We set the property context.response.body on the context object and retrieve all entities available in the database. For a productive scenario, you would implement paging at this point.

In the update method, we first read the body of the HTTP request. In return, we get an object with the property value. There you will find a JSON object, which we will later send to the API to change a record. Then, we check if we can find a GameEntity with the given id in the database. If this is not the case, we return from the method early. Since we do not set a response, Oak will automatically generate an HTTP error 404 here. If we find the entity in the database, we update it according to all values that were transmitted. Again, please note that you would not implement it like this in a production scenario, because you first have to check all values that were transferred to you for validity. After the update, we call our save method so that the change is also persisted. Finally, we set the HTTP status to 200 and tell the client that the update was successful. If something went wrong, our try-catch mechanism will take action, log the error to the console, and end the request with an HTTP error 500.

In order to use our controller, we add the file src/http-server.ts. We find the addition in Listing 7.

Listing 7: Addition of the file src/http-server.ts

// add to import
import { GameController } from "./controllers/game.controller.ts";
 
// const router = new Router();
new GameController(router);
// this.app.use ...

This completes the development of our HTTP API. If we try to start the application, we are greeted with an error message. The SQLite 3 database is read from and written to disk. For this, we have to give Deno explicit permission by specifying the flags –allow-read and –allow-write at startup. Our start command looks like this:

deno run --allow-net --allow-env --allow-read --allow-write index.ts

For example, via Postman [9] an HTTP request can be used to create a GameEntity (Listing 8).

Listing 8: HTTP POST request to create a GameEntity

POST /games HTTP/1.1
Host: localhost:8080
Content-Type: application/json
 
{
  "name": "Idle Ambulance",
  "type": "Idle Game",
  "developer": "Boundfox Studios",
  "publisher": "Boundfox Studios"
}

Unit testing

To make sure that a URL /games is created, we can develop a small unit test. Deno comes with a test framework and a test runner. To do this, we create the file src/controllers/game.controller.test.ts with the contents of Listing 9.

Listing 9: Content of the file src/controllers/game.controller.test.ts

import { stub } from "https://raw.githubusercontent.com/udibo/mock/v0.3.0/stub.ts";
import { GameController } from "./game.controller.ts";
import { Router } from "https://deno.land/x/[email protected]/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
 
Deno.test("create post route", () => {
  const router = new Router();
  const postStub = stub(router, "post");
  const sut = new GameController(router);
 
  assertEquals(postStub.calls.length, 1);
  assertEquals(postStub.calls[0].args[0], "/games");
});



In listing 9, we see a small unit test together with the module mock for stubbing objects. Via Deno.test we can create a unit test that is executed with a callback for testing. The callback can also return a promise so that the async/await pattern can be used. In the callback itself, we create an instance of Router and stub the method post. Unfortunately, mock can’t really mock yet, as you know it from ts-mockito for example, so we have to make do with a stub first. Then we create our GameController and put the router in. Remember that when you create a GameController, the routes are defined. Via our stub postStub we can check if the method was called and if the first argument of the method corresponds to the route /games.

On a command line, we can call deno test to automatically run all tests in the project. Deno only finds files that end in .test or _test.

Lock file and integrity check

Let’s take a look at a few more features that Deno brings to the table in this article. For this purpose, we take another look at the module system, which may take some getting used to with its URL import, especially since versioning is also included in the URL. This means that when we update a library, we actually have to change many files, whereas for Node.js we only changed the version in the package.json.

At Deno we can do something similar. To do this, create a file deps.ts, in which you load and export all external dependencies, as shown in Listing 10. This has another advantage. We can have Deno create a lock file. A file hash is stored in this lock file, so that we can make sure that there was no change to this module in case a module is downloaded again. The lock file, like the package-lock.json, is also checked into the source control system. The lock file is created as follows:

deno cache --lock lock.json --lock-write src/deps.ts

The –lock flag specifies the freely selectable name of the lock file. With –lock-write we inform you that we want to write the lock file. To check if everything is correct or if the project is checked out again, we can use the following command:

deno cache -r --lock lock.json src/deps.ts

On the one hand, we specify the file name of our lock file again. The -r flag tells Deno to reload and cache all modules. The lock file is then used to check whether you have exactly the same copies on your computer as when the lock file was created.

Listing 10: Example content of a deps.ts file

export { Router, RouterContext, Application } from "https://deno.land/x/[email protected]/mod.ts";
export { Database, DATA_TYPES, Model } from "https://deno.land/x/denodb/mod.ts";
export { assertEquals } from "https://deno.land/std/testing/asserts.ts";
export { stub } from "https://raw.githubusercontent.com/udibo/mock/v0.3.0/stub.ts";

Debugging

Deno supports the V8 inspector protocol for debugging [10]. This protocol is implemented by WebStorm, VS Code, and by Chrome itself. All these programs can be used for debugging. To do this, Deno must be started with the –inspect or –inspect-brk flag. In Chrome, the page chrome://inspect can then be called. There you will find a remote target for debugging the Deno application with the Chrome DevTools. Please note that debugging support is currently still very shaky. It is possible that the connection to the application will be terminated or the application itself will simply end. In future Deno versions, the debugging should become much more stable.

Code bundler

Deno comes with a bundler that accepts an entry point to the application and combines the complete application, including all dependencies, into a single JavaScript file as ES modules [11]. This bundle can then either be started via deno run, consumed from other files, or with the browser. Our application from this article can be packed with the following command:

deno bundle index.ts app.bundle.js

Code Formatter

The Code Formatter is another practical tool from Deno that can format our TypeScript files [12]. To do this, you can call deno fmt on the command line to format the entire project. If you only want to format a single file or directory, you can alternatively specify them as arguments. In its current state, the Code Formatter of Deno cannot be configured with custom code styles yet. There are already issues and pull requests to be able to use the options of the Prettier formatter in the future. Again, it is a matter of time before these things are implemented.

Dependency inspector

The Dependency Inspector rounds off the tools around Deno in this article [13]. The Dependency Inspector recursively lists all dependencies of a file, so that you can see at any time which module is loaded and needed by which code. With the following command line command the Inspector can be used for local files as well as for URLs:

deno info index.ts

Conclusion

Even though Deno is still in its infancy in version 1.0, many concepts are clearly recognizable, including those where Node.js was far too negligent. It will be exciting to see how Deno is developed further and, above all, how it is accepted in the real world. With this article, we just scratched the surface of Deno, implemented a first small API together with unit testing, and got to know the most important tools. There is still much to explore, e.g. the in-house documentation generator, the compiler API, the script installer [14], the use of webworkers [15], and the execution of WebAssembly binaries directly in Deno [16].

In the future, will Deno replace Node.js? Right now, this question remains open. As the Deno ecosystem matures, projects face the decision of whether to use Node.js or Deno. Should Node.js no longer offer any advantages over Deno, the choice will probably be in favor of Deno. Bye-bye Node.js, hello Deno!

I myself will implement new small projects with Deno rather than Node.js, because I have been using TypeScript in Node for a long time. I get a better workflow with Deno. As soon as the integration into IDEs is better, nothing will stand in the way of Deno. I am curious to see where the journey will take us. Have fun trying out Deno!

Links & Literature

[1] https://deno.land
[2] https://github.com/thinktecture-labs/windows-developer-deno
[3] https://github.com/denoland/deno_install
[4] https://asdf-vm.com
[5] https://marketplace.visualstudio.com/items?itemName=justjavac.vscode-deno
[6] https://youtrack.jetbrains.com/issue/WEB-41607#focus=streamItem-27-4146419.0-0
[7] https://github.com/alosaur/alosaur
[8] https://deno.land/x/denodb
[9] https://www.postman.com
[10] https://deno.land/manual/tools/debugger
[11] https://deno.land/manual/tools/bundler
[12] https://deno.land/manual/tools/formatter
[13] https://deno.land/manual/tools/dependency_inspector
[14] https://deno.land/manual/tools
[15] https://deno.land/manual/runtime/workers
[16] https://deno.land/manual/getting_started/webassembly

Top Articles About Node.js

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows

DON'T MISS ANY NEWS