Deno VS Node
and the neverending challenge of JavaScript runtimes

Ryan Dahl, the creator of Node.js, has spent the last few years working on Deno, the new Javascript runtime that, according to Ryan himself, should solve all the problems that plague Node.js.
Don’t get me wrong, Node.js is a great runtime, mainly thanks to the huge userland modules and the use of JavaScript; however Dahl himself admitted that he did not think about some fundamental aspects such as security, modules and dependencies, just to name a few.
It is to be said in defense of Ryan, who in his time would never have imagined that the platform would grow so much, in such a short period.
It must be said, that in 2009, JavaScript was still that language that everyone made fun of and many of the features that are present today were not there.
What is Deno?
Deno is a TypeScript runtime based on V8, the Google’s JavaScript runtime; if you are familiar with Node.js, the popular server-side JavaScript ecosystem, you will understand that Deno is exactly the same. Except that it was designed with some improvements:
- It is based on the modern functionality of the JavaScript language;
- It has an extensive Standard library;
- It supports TypeScript natively;
- Supports EcmaScript modules;
- It doesn’t have a centralized package manager like npm;
- It has several built-in utilities such as a dependency inspector and a code formatter;
- Aims to be as compatible with browsers as possible;
- Security is the main feature.
What are the main differences with Node.js?
I think that Deno’s main goal is to replace Node.js. However, there are some important common characteristics. For example:
- Both were created by Ryan Dahl;
- Both were developed on Google’s V8 engine;
- Both were developed to execute server-side JavaScript.
But on the other side there are some important differences:
- Rust and TypeScript. Unlike Node.js which is written in C++ and JavaScript, Deno is written in Rust and TypeScript.
- Tokyo. Introduced in place of libuv as an event-driven asynchronous platform.
- Package Manager. Unlike Node.js, Deno doesn’t have a centralized package manager, so it is possible to import any ECMAScript module from a url.
- ECMAScript. Deno uses modern ECMAScript functionality in all its APIs, while Node.js uses a standard callback-based library.
- Security. Unlike a Node.js program which, by default, inherits the permissions from the system user that’s running the script, a Deno program runs in a sandbox. For example, the access to file system, to network resources, etc., must be authorized with a flag permission.
Installation
Deno is a single executable file without dependencies. We can install it on our machine by downloading the binary version from this page, or we can download and execute one of the installers listed below.
Shell (Mac, Linux)
$ curl -fsSL https://deno.land/x/install/install.sh | sh
PowerShell (Windows)
$ iwr https://deno.land/x/install/install.ps1 -useb | iex
Homebrew (Mac OS)
$ brew install deno
Let’s take a look at security
One of the main Deno’s features is the security. Compared to Node.js, Deno executes the source code in a sandbox, this mean that the runtime:
- Doesn’t have access to the file system;
- Doesn’t have access to the network resources;
- Cannot excecute other scripts;
- Doesn’t have access to environment variables.
Let’s make a simple example. Consider the following script:
async function main () {
const encoder = new TextEncoder ()
const data = encoder.encode ('Hello Deno! 🦕 \n')
await Deno.writeFile( 'hello.txt' , data)
}
main()
The script is really simple. It just create a text file named hello.txt
that will contain the string Hello Deno
🦕. Really simple! Or not?
As we said before, the code will run in a sandbox and, obviously, it doesn’t have the access to the filesystem. Infact, if we execute the script with the following command:
$ deno run hello-world.ts
It will print on terminalsomething like:
Check file:///home/davide/denoExample/hello-world.ts
error: Uncaught PermissionDenied: write access to "hello.txt", run again with the --allow-write flag
at unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)
at Object.sendAsync ($deno$/ops/dispatch_json.ts:93:10)
at async Object.open ($deno$/files.ts:38:15)
at async Object.writeFile ($deno$/write_file.ts:61:16)
at async file:///home/davide/projects/denoExample/hello-world.ts:5:3
As we can see, the error message is really clear. The file was not created on the filesystem because the script does not have the write permission to do that but, by adding the flag --allow-write
:
$ deno run --allow-write hello-world.ts
the script will end without errors and the file hello.txt
was created correctly in the current working directory.
In addition to the flag --allow-write
that give to us the access to the filesystem, there are also other flags such as --allow-net
that give to us the access to the network resources, or --allow-run
that is useful to run external script or subprocess. We can find the complete permissions list at the following url https://deno.land/manual/getting_started/permissions.
A simple server
Now we will create a simple server that accept connections on port 8000
and return to the client the string Hello Deno
.
// file server.ts
import { serve } from 'https://deno.land/std/http/server.ts'const s = serve({ port: 8000 })
console.log('Server listening on port :8000')
for await (const req of s) {
req.respond({ body: 'Hello Deno!\n' })
}
Obviously to run the script we need to specify the --allow-net
flag:
$ deno run --allow-net server.ts
In our terminal will appear something like:

Now if we open our favourite browser, or if we want to use the curl
command, we can take a test to the URL http://localhost:8000
. The result will be something like this:

Modules
Just like browsers, Deno loads all his modules via URL. Many people are initially confused by this approach, but that make sense. Here an example:
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Importing packages via URL has advantages and disvantages. The main advantages are:
- more flexibility;
- we can create a package without publish it in a public repository (like npm).
I think that a sort of package manager can be released in future, but nothing official has come out for now.
The official Deno website give to us the opportunity to host our source code, and then the distribution via URLs: https://deno.land/x/.
Importing packages via URLs, give to the developers the freedom they need to host their code wherever they want: the decentralization at best. Therefore, we don’t need a package.json
file or the node_modules
directory.
When the application start, all imported packages are downloaded, compiled, and stored a cache memory. If we want to download all the packages again we need to specify the flag --reload
.
I need to type the URL every time? 🤯🤬
Deno support import maps natively. This mean that it’s possible to specify a special command flag like --importmap=<FILENAME>
. Let’s take a look to a simple example.
Imagine that we have a file import_map.json
, with the following content:
{
"imports": {
"fmt/": "https://deno.land/std@0.65.0/fmt/"
}
}
The file specifies that at /ftm
key, of the imports
object, correspond the URL https://deno.land/sdt@0.65.0/ftm/
and it can be used as follow:
// file colors.ts
import { green } from "fmt/colors.ts";console.log(green("Hello Deno! 🦕"));
This feature is unstable at the moment, and we need to run our script color.ts
using the flag --unstable
, so:
$ deno run --unstable --importmap=import_map.json colors.ts
Now in our terminal appear something like this:

Versioning
The package versioning is a developer responsibility and, on the client side, we can decide to use a specific version in the URL of the package when we import it:
https://unpkg.com/package-name@0.0.5/dist/package-name.js
Ready to use utilities
Speaking honestly: the current state of JavaScript tools for developer is a real CHAOS! And when TypeScript ones are added, the chaos increase further. 😱

One of the best JavaScript feature is that the code is not compiled, and it can be executed immediately in a browser. This make the life easier for a developer and it is very easy to get immediate feedback on written code. Unfortunately, however, this simplicity in the last period has been undetermined by what I consider “The cult of excessive instruments”. Theese tools have turned JavaScript development into a real nightmare of complexity. There are entire online courses for Webpack configuration guide! Yes, you got it right…a whole course!
The chaos of the tools has increased to the point that many developers are eager to get back to actually writing code rather than playing with configuration files. An emerging project that aim to resolve this problem its Facebook’s Rome project.
Deno, on the other hand, has an entire and complete ecosystem, such as runtime and modules management. This approach offer to the developer all the tools they need to build their applications. Now, let’s take a look at the tools that Deno 1.6 ecosystem offer, and how the developers can use them to reduce third party dependencies and simplify the development.
It’s not possible to replace an entire build pipeline in Deno, but I think that we don’t think we’ll wait much longer before we have it. Below the list of integrated features:
- bundler: it write in a single JavaScript file the specified module and all its dependencies;
- debugger: it give to us the ability to debug our Deno program with Chrome Devtools, VS Code and other tools;
- dependency inspector: if we execute this tool on a ES module it show all the dependencies tree;
- doc generator: it analyze all the JSDoc annotation in a given file and produce the documentation for us;
- formatter: it format the JavaScript, or TypeScript, code automatically;
- test runner: it’s an utility that give to us the ability to test our source code using the
assertions
module of the standard library. - linter: useful for identifying potential bugs in our programs.
Bundler
Deno can create a simple bundle from command line using deno bundle
command, but it expose an API internally. With this API the developer can create a custom output, or something that can be used for frontend purpose. This API is instable, so we need to use the --unstable
flag. Let’s take the example we did earlier, modifying it as follows:
// file colors.ts
import { green } from "https://deno.land/std@0.65.0/fmt/colors.ts";console.log(green("Hello Deno! 🦕"));
And now let’s create our bundle from command line:
$ deno bundle colors.ts colors.bundle.js
this command create a file colors.bundle.js
that will contains all the source code that we need to execute it. In fact if we try to run the script with command:
$ deno run colors.bundle.js
we will notice that no module will be downloader from Deno’s repository, this because all the code needed for the execution is contained in the colors.bundle.js
file. The result that we will see on the terminal is the same of the previous example:

Debugger
Deno has an integrated debugger. If we want to launch a program in debug mode manually we need to use the --inspect-brk
$ deno run -A — inspect-brk fileToDebug.ts
Now if we open Chrome inspector chrome://inspect
we find a page similar to this

If we click on inspect
we can start to debug our code.
Dependency inspector
Use this tool it’s really simple! Simply we need to use the info
subcommand followed and URL (or path) of a module and it will print the dependency tree of that module. If we launch the command using theserver.ts
file used in the previous example, it will print in our terminal someting like this:

Also the command deno info
can be used to show cache information:

Doc generator
This is a really useful utility that allows us to generate the JSDoc automatically. If we want to use it just run the command deno doc
, followed by a list of one or more source files, and automatically it will printed to terminal all the documentation for all the exported files of our modules. Let’s take a look on how it works with a simple example. Let’s imagine tha we have a file add.ts
with following content:
/**
* Adds x and y.
* @param {number} x
* @param {number} y
* @returns {number} Sum of x and y
*/
export function add(x: number, y: number): number {
return x + y;
}
Executing the deno doc
command, it will printed the following JSDoc on the standard output:

It’s possible tu use the --json
flag to produce a JSON format documentation. This JSON format can be used by Deno’s website to generate the module documentation automatically.
Formatter
Formatter is provided by dprint, an alternative to Prettier, that clone all the rules enstabished by Prettier 2.0. If we want to format one or more files, we can use deno ftm <files>
or a VSCode extension. If we run the command with the --check
flag, will be executed the format check of all JavaScript and TypeScript files in the current working directory.
Test runner
The syntax of this utility it’s really simple. We just need to use the deno test
command and it will be executed the tests for all files that ends with _test
or .test
with the .js
, .ts
, .tsx
or .jsx
extensions. In addition to this utility it’s possible to use the standard Deno API that give to us the asserts
module that we can use in the following way:
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"
Deno.test({
name: "testing example",
fn(): void {
assertEquals("world", "world")
assertEquals({ hello: "world" }, { hello: "world" })
},
})
This module give to us nine assertions that we can use in our test cases:
assert(expr: unknown, msg = ""): asserts expr
assertEquals(actual: unknown, expected: unknown, msg?: string): void
assertNotEquals(actual: unknown, expected: unknown, msg?: string): void
assertStrictEquals(actual: unknown, expected: unknown, msg?: string): void
assertStringContains(actual: string, expected: string, msg?: string): void
assertArrayContains(actual: unknown[], expected: unknown[], msg?: string): void
assertMatch(actual: string, expected: RegExp, msg?: string): void
assertThrows(fn: () => void, ErrorClass?: Constructor, msgIncludes = "", msg?: string): Error
assertThrowsAsync(fn: () => Promise<void>, ErrorClass?: Constructor, msgIncludes = "", msg?: string): Promise<Error>
Linter
Deno has an integrated JavaScript and TypeScript linter. This is a new feature and it’s instable and, obviously, if we want to use it require the --unstable
flag to execute it.
# This command lint all the ts and js files in the current working directory
$ deno lint --unstable# This command lint all the listed files
$ deno lint --unstable myfile1.ts myfile2.ts
Benchmark
Ok folks! We’ve arrived to the moment of truth! Who’s the best JavaScript enviroment Deno or Node? But I think that the correct question is another: Who’s the fastest? I did a really simple benchmark (an http hello server) and the results were very interesting. I made them on my laptop that has the following characteristics:
Model: XPS 13 9380
Processor: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
RAM: 16GB DDR3 2133MHz
OS: Ubuntu 20.04 LTS
Kernel version: 5.4.0-42
The tool that I used to make these benchmark is autocannon and the used scripts are the following:
// file node_http.js
const http = require("http");const hostname = "127.0.0.1";
const port = 3000;http.createServer((req, res) => {
res.end("Hello World");
}).listen(port, hostname, () => {
console.log("node listening on:", port);
});
// file deno_http.ts
import { serve } from "https://deno.land/std@0.61.0/http/server.ts";const port = 3000;
const s = serve({ port });
const body = new TextEncoder().encode("Hello World");console.log("deno_http listen on", port);
for await (const req of s) {
const res = {
body,
headers: new Headers(),
};
res.headers.set("Date", new Date().toUTCString());
res.headers.set("Connection", "keep-alive");
req.respond(res).catch(() => {});
}
We can find them in the following github repository:
The first test case was performed on 100 concurrent connections with the command autconannon http://localhost:3000 -c100
and the results are the sequent:

It seems that Node beat Deno in velocity! But this benchmark is based on 100 concurrent connections which are many for a small or medium server. So let’s do another test: this time with 10 concurrent connections.
And again, Node beats Deno:

Seems that, in term of performance, Node beats Deno 2–0! It performs better in both analyzed cases. However, the Deno it’s a really young project and the community is working hard to get adopted in the production environment soon, but it will be a tough fight again a titan like Node.
Conclusion
The main purpose of this article was not to support either Node or Deno, but rather to compare the two enviroments. Now you should have an understanding of the similarities and differences between the two.
Deno has some particular advantages for developers, including a robust suppport system and natively TypeScript support. The design decisions and additional built-in tools aim to provide a productive environment system and a good developer experience. I don’t know if these choices can be a double-edged sword in the future, but seems to attract more developers right now.
Node, on the other hand, has a robust ecosystem, ten years of development and releases behind it, an oceanic community and online courses that can help us on many threads or problems, an infinite list of frameworks (Fastify, Express, Hapi, Koa etc.), many books like “Node.js Design Patterns” or “Node Cookbook” that I consider the best books that talks about Node.js. For these, and many other reasons, I think that Node is the most secure choice to make for now. What can I say…
HAPPY CODING!