An Introduction to Deno: Is It Better than Node.js?
Tags: deno, node
Published: 2022-02-16 12:40:58

Deno is a JavaScript and TypeScript runtime similar to Node.js, built on Rust and the V8 JavaScript engine. It was created by Ryan Dahl, the original inventor of Node.js, to counter mistakes he made when he originally designed and released Node.js back in 2009.

Ryan's regrets about Node.js are well documented in [his famous '10 Things I Regret About Node.js' talk](https://www.youtube.com/watch?v=M3BM9TB-8yA) at JSConf EU in 2018. To summarize, he bemoaned the lack of attention to security, module resolution through `node_modules`, various deviations from how browser's worked, amongst other things, and he set out to fix all these mistakes in Deno.

In this article, we'll discuss why Deno was created and its advantages and disadvantages compared to Node.js. It'll also give a practical overview of Deno's quirks and features so that you can decide if it's a good fit for your next project.

## Installing Deno

[Deno](https://deno.land/) is distributed as a single, self-contained binary without any dependencies.

You can [install Deno](https://deno.land/manual@v1.14.2/getting_started/installation) in various ways, depending on your operating system. The simplest method involves downloading and executing a shell script as shown below:

```bash
# Linux and macOS
$ curl -fsSL https://deno.land/x/install/install.sh | sh

# Windows PowerShell
$ iwr https://deno.land/x/install/install.ps1 -useb | iex
```

Once you've executed the appropriate command for your operating system, the Deno CLI binary will be downloaded to your computer. You may be required to add the binary location to your `PATH`, depending on the installation method you chose.

You can do this in Bash by adding the lines below to your
`$HOME/bash_profile` file. You may need to start a new shell session for the changes to take effect.

```bash
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
```

To verify the installed version of Deno, run the command below. It should print the Deno version to the console if the CLI was downloaded successfully and added to your `PATH`.

```bash
$ deno --version
deno 1.14.2 (release, x86_64-unknown-linux-gnu)
v8 9.4.146.16
typescript 4.4.2
```

If you have an outdated version of Deno, upgrading to the latest release can be done through the `upgrade` subcommand:

```bash
$ deno upgrade
Looking up latest version
Found latest version 1.14.2
Checking https://github.com/denoland/deno/releases/download/v1.14.2/deno-x86_64-unknown-linux-gnu.zip
31.5 MiB / 31.5 MiB (100.0%)
Deno is upgrading to version 1.14.2
Archive:  /tmp/.tmpfdtMXE/deno.zip
  inflating: deno
Upgraded successfully
```

Go ahead and write a customary hello world program to verify that everything works correctly. You can create a directory for your Deno programs and place the following code in an `index.ts` file at the directory's root.

```typescript
function hello(str: string) {
  return `Hello ${str}!`;
}

console.log(hello("Deno"));
```

Save and execute the file by providing the filename as an argument to the `run` subcommand. If the text "Hello Deno!" outputs, it means that you have installed and set up Deno correctly.

```bash
$ deno run index.ts
Check file:///home/ayo/dev/deno/index.ts
Hello Deno!
```

To find out about other features and options provided by the Deno CLI, use the `--help` flag:

```bash
$ deno --help
```

## Deno's First-class TypeScript Support

One of the big selling points of Deno over Node.js is its first-class support for TypeScript.

As you've already seen, you don't need to do anything besides install the Deno CLI for it to work. Like its predecessor, Deno uses the V8 runtime engine under the hood to parse and execute JavaScript code, but it also includes the [TypeScript compiler](https://github.com/microsoft/TypeScript) in its executable to achieve TypeScript support.

Under the hood, TypeScript code is checked and compiled. The resulting JavaScript code is cached in a directory on your filesystem, ready to be executed again without being compiled from scratch. You can use `deno info` to inspect the location of the cache directory and other directories containing Deno-managed files.

Deno does not require any configuration to work with TypeScript, but you can provide one if you want to tweak how the TypeScript compiler parses the code. You can provide a JSON file to specify the TypeScript compiler options. Although `tsconfig.json` is the convention when using the standalone `tsc` compiler, the Deno team recommends using `deno.json` because other Deno-specific configuration options can be placed there.

Note that Deno doesn't support all TypeScript compiler options. [A full list](https://deno.land/manual@v1.14.2/typescript/configuration#how-deno-uses-a-configuration-file) of the available options, along with their default values, are presented in the Deno documentation. Here's a sample configuration file for Deno:

```json
{
  "compilerOptions": {
    "checkJs": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noUncheckedIndexedAccess": true
  }
}
```

At the time of writing, Deno does not automatically detect a `deno.json` file so it must be specified via the `--config` flag. However, this feature is planned for a future release.

```bash
$ deno run --config deno.json index.ts
```

When the Deno CLI encounters a type error, it halts the compilation of the script and terminates with a non-zero exit code. You can bypass the error by:

- using `//@ts-ignore` or `//@ts-expect-error` at the point where the error occurred or
- `// @ts-nocheck` at the beginning of the file to ignore all errors in a file.

Deno also provides a `--no-check` flag to disable type checking altogether. This helps prevent the TypeScript compiler from slowing you down when iterating quickly on a problem.

```bash
$ deno run --no-check index.ts
```

## Permissions in Deno

Deno prides itself on being a secure runtime for JavaScript and TypeScript. Part of the way it maintains security is through its [permissions system](https://deno.land/manual@v1.14.2/getting_started/permissions). To demonstrate how permissions work in Deno, add the below script to your `index.ts` file. It's a script that fetches the [latest global Covid-19 statistics from disease.sh](https://disease.sh/docs/).

```typescript
async function getCovidStats() {
  try {
    const response = await fetch("https://disease.sh/v3/covid-19/all");
    const data = await response.json();
    console.table(data);
  } catch (err) {
    console.error(err);
  }
}

getCovidStats();
```

When you attempt to execute the script, it should display a `PermissionDenied` error:

```bash
$ deno run index.ts
PermissionDenied: Requires net access to "disease.sh", run again with the --allow-net flag
```

The error message above indicates that the script hasn't been granted network access. It suggests including the `--allow-net` flag in the command to grant access.

```bash
$ deno run --allow-net index.ts
┌────────────────────────┬───────────────┐
│ (idx)                  │ Values        │
├────────────────────────┼───────────────┤
│ updated                │ 1633335683059 │
│ cases                  │     235736138 │
│ todayCases             │         32766 │
│ deaths                 │       4816283 │
│ todayDeaths            │           670 │
│ recovered              │     212616434 │
│ todayRecovered         │         51546 │
│ active                 │      18303421 │
│ critical               │         86856 │
│ casesPerOneMillion     │         30243 │
│ deathsPerOneMillion    │         617.9 │
│ tests                  │    3716763329 │
│ testsPerOneMillion     │     473234.63 │
│ population             │    7853954694 │
│ oneCasePerPeople       │             0 │
│ oneDeathPerPeople      │             0 │
│ oneTestPerPeople       │             0 │
│ activePerOneMillion    │       2330.47 │
│ recoveredPerOneMillion │      27071.26 │
│ criticalPerOneMillion  │         11.06 │
│ affectedCountries      │           223 │
└────────────────────────┴───────────────┘
```

Instead of granting blanket approval for the script to access all websites (as shown above), you can provide an allowlist of comma-separated hostnames or IP addresses as an argument to `--allow-net` so that only the specified websites are accessible by the script. If the script tries to connect to a domain that is not in the allowlist, Deno will prevent it from connecting, and the script execution will fail.

```bash
$ deno run --allow-net='disease.sh' index.ts
```

This feature is one of Deno's improvements over Node.js where
any script can access any resource over the network. Similar permissions also exist for reading from and writing to the filesystem. If a script needs to perform either task, you need to specify the `--allow-read` and `--allow-write` permissions, respectively. Both flags allow you to set the specific directories accessible to a script so that other parts of the filesystem are safe from tampering. Deno also provides an `--allow-all` flag that enables all security-sensitive functions for a script, if so desired.

## Deno's Compatibility With Browser APIs

One of Deno's main [goals](https://deno.land/manual@v1.14.2/introduction#goals) is to be compatible with web browsers, where possible. This is reflected in its use of web platform APIs instead of creating a Deno-specific API for certain operations. For example, we saw the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) in action in the previous section. This is the exact Fetch API used in browsers, with a few deviations where necessary to account for the unique security model in Deno (and these changes are mostly inconsequential).

[There's a list of all implemented browser APIs in Deno's online documentation](https://deno.land/manual@v1.14.2/runtime/web_platform_apis).

## Dependency Management in Deno

The way Deno manages dependencies is probably the most obvious way it diverges significantly from Node.js.

Node.js uses a package manager like `npm` or `yarn` to download third-party packages from the [npm registry](https://npmjs.com) into a `node_modules` directory and a `package.json` file to keep track of a project's dependencies. Deno does away with those mechanisms in favor of a more browser-centric way of using third-party packages: URLs.

Here's an example that uses [Oak](https://github.com/oakserver/oak), a web application framework for Deno, to create a basic web server:

```typescript
import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello Deno!";
});

app.addEventListener("listen", ({ hostname, port, secure }) => {
  console.log(`Listening on: http://localhost:${port}`);
});

await app.listen({ port: 8000 });
```

Deno uses ES modules, the same module system used in web browsers. A module can be imported from an absolute or relative path, as long as the referenced script exports methods or other values. It's worth noting that the file extension must always be present, regardless of whether you import from an absolute or relative path.

While you can import modules from any URL, many third-party modules specifically built for Deno are cached through [deno.land/x](https://deno.land/x). Each time a new version of a module is released, it is automatically cached at that location and made immutable so that the contents of a specific version of a module remain unchangeable.

Suppose you run the code in the previous snippet. In that case, it will download the module and all its dependencies and cache them locally in the directory specified by the `DENO_DIR` environmental variable (the default should be `$HOME/.cache/deno`). The next time the program runs, there will be no downloads since all the dependencies have been cached locally. This is similar to how the [Go module](https://go.dev/blog/using-go-modules) system works.

```bash
$ deno run --allow-net index.ts
Download https://deno.land/x/oak/mod.ts
Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts
Download https://deno.land/x/oak@v9.0.1/mod.ts
. . .
```

For production applications, the creators of Deno [recommend](https://deno.land/manual@v1.14.2/linking_to_external_code#but-what-if-the-host-of-the-url-goes-down-the-source-won#39t-be-available) vendoring your dependencies by checking them into source control to ensure continued availability (even if the source of the module is unavailable, for whatever reason). Point the `DENO_DIR` environmental variable to a local directory in your project (such as `vendor`), which you can commit to Git.

For example, the command below will download all your script's dependencies into a `vendor` directory in your project. You can subsequently commit the folder to pull it down all at once in your production server. You'll also need to set the `DENO_DIR` variable to read from the `vendor` directory on the server, instead of downloading them all over again.

```bash
$ DENO_DIR=$PWD/vendor deno cache index.ts # Linux and macOS
$ $env:DENO_DIR="$(get-location)\vendor"; deno cache index.ts # Windows PowerShell
```

Deno also supports the concept of versioning your dependencies to ensure reproducible builds. At the moment, we've imported Oak from `https://deno.land/x/oak/mod.ts`. This always downloads the latest version, which could become incompatible with your program in the future. It also causes Deno to produce a warning when you download the module for the first time:

```bash
Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts
```

It is considered best practice to reference a specific release as follows:

```bash
import { Application } from 'https://deno.land/x/oak@v9.0.1/mod.ts';
```

If you're referencing a module in many files in your codebase, upgrading it may become tedious since you have to update the URL in many places. To circumvent this issue, the Deno team recommends [importing your external dependencies in a centralized `deps.ts` file and then re-exporting them](https://deno.land/manual@v1.14.2/linking_to_external_code#it-seems-unwieldy-to-import-urls-everywhere). Here's a sample `deps.ts` file that exports what we need from the Oak library.

```typescript
export { Application, Router } from "https://deno.land/x/oak@v9.0.1/mod.ts";
```

Then in your application code, you can import them as follows:

```typescript
import { Application, Router } from "./deps.ts";
```

At this point, updating a module becomes a simple matter of changing the URL in the `deps.ts` file to point to the new version.

## The Deno Standard Library

Deno provides a [standard libary](https://deno.land/std) (stdlib) that aims to be a loose port of [Go's standard library](https://golang.org/pkg/). The modules contained in the standard library are audited by the Deno team and updated with each release of Deno. The intention behind providing a stdlib is to allow you to create useful web applications right away, without resorting to any third-party packages (as is the norm in the Node.js ecosystem).

Some examples of standard library modules you might find helpful include:

- [HTTP](https://deno.land/std@0.109.0/http): An HTTP client and server implementation for Deno.
- [Fmt](https://deno.land/std@0.109.0/fmt): Includes helpers for printing formatted output.
- [Testing](https://deno.land/std@0.109.0/testing): Provides basic utilities for testing and benchmarking your code.
- [FS](https://deno.land/std@0.109.0/fs): Has helpers for manipulating the filesystem.
- [Encoding](https://deno.land/std@0.109.0/encoding): Provides helpers to deal with various file formats, such as XML, CSV, base64, YAML, binary, and more.
- [Node](https://deno.land/std@0.110.0/node): Has a compatibility layer for the Node.js standard library.

Here's an example (taken from [Deno's official docs](https://deno.land/manual@v1.14.2/examples/http_server#using-the-codestdhttpcode-library)) that utilizes the `http` module in Deno's stdlib to create a basic web server:

```typescript
import { listenAndServe } from "https://deno.land/std@0.109.0/http/server.ts";

const addr = ":8080";

const handler = (request: Request): Response => {
  let body = "Your user-agent is:\n\n";
  body += request.headers.get("user-agent") || "Unknown";

  return new Response(body, { status: 200 });
};

console.log(`HTTP webserver running. Access it at: http://localhost:8080/`);
await listenAndServe(addr, handler);
```

Start the server through the command below:

```bash
$ deno run --allow-net index.ts
HTTP webserver running. Access it at: http://localhost:8080/
```

In a different terminal, access the running server through the following command:

```bash
$ curl http://localhost:8080
Your user-agent is:

curl/7.68.0
```

Note that the modules in the standard library are currently tagged as unstable (as reflected in the version number). This means you should not rely on them just yet for a serious production application.

## Using NPM Packages in Deno

It cannot be denied that one of the major reasons why Node.js has been so successful is the large number of packages that can be downloaded and utilized in a project. If you're considering switching to Deno, you may be wondering if you'd have to give up all the NPM packages you know and love.

The short answer is: no. While you may not be able to utilize some NPM packages in Deno if they rely on Node.js APIs (especially if the specific APIs are not supported in [Deno's Node.js compatibility layer](https://deno.land/std/node)), many NPM packages can be utilized in Deno through CDNs like
[esm.sh](https://esm.sh/) and [skypack.dev](https://www.skypack.dev/). Both these CDNs provide NPM packages as ES Modules that can be subsequently consumed in a Deno script even if the author of the package did not design it to target Deno specifically.

Here's an example that imports the [dayjs NPM package](https://www.npmjs.com/package/dayjs) from Skypack in a Deno script:

```typescript
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7";

console.log(`Today is: ${dayjs().format("MMMM DD, YYYY")}`);
```

```bash
$ deno run index.ts
Today is: October 05, 2021
```

To ensure that Deno can discover the types associated with a package, ensure you add the `?dts` suffix at the end of the package URL when using Skypack's CDN. This causes Skypack to set a `X-TypeScript-Types` header so that Deno can automatically discover the types associated with a package. Esm.sh includes this header by default, but you can opt-out by adding the `?no-check` suffix at the end of the package URL.

```typescript
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7?dts";
```

## Deno Tooling

The Deno CLI comes with several valuable tools that make the developer experience much more pleasant. Like Node.js, it comes with a REPL (Read Evaluate Print Loop), which you can access with `deno repl`.

```bash
$ deno repl
Deno 1.14.2
exit using ctrl+d or close()
> 2+2
4
```

It also has a built-in [file watcher](https://deno.land/manual@v1.14.2/getting_started/command_line_interface#watch-mode) that can be used with several of its subcommands. For example, you can configure `deno run` to automatically rebuild and restart a program once a file is changed by using the `--watch` flag. In Node.js, this functionality is generally achieved through some third-party package such as [nodemon](https://www.npmjs.com/package/nodemon).

```bash
$ deno run --allow-net --watch index.ts
HTTP webserver running. Access it at: http://localhost:8080/
Watcher File change detected! Restarting!
HTTP webserver running. Access it at: http://localhost:8080/
```

With [Deno 1.6](https://deno.com/blog/v1.6), you can compile scripts into self-contained executables that do not require Deno to be installed through the `compile` subcommand (you can use [pkg](https://github.com/vercel/pkg) to do the same in Node.js). You can also generate executables for other platforms ([cross compilation](https://deno.land/manual@v1.14.2/tools/compiler#cross-compilation)) through the `--target` flag. When compiling a script, you must specify the permissions needed for it to run.

```bash
$ deno compile --allow-net --output server index.ts
$ ./server
HTTP webserver running. Access it at: http://localhost:8080/
```

Note that the binaries produced through this process are quite huge. In my testing, `deno compile` produced an 83MB binary for a simple "Hello world" program. However, the Deno team is currently working on a way to reduce the file sizes to be a lot more manageable.

Another way to distribute a Deno program is to package it into a single JavaScript file through the `bundle` subcommand. This file contains the source code of the program and all its dependencies, and it can be executed through `deno run` as shown below.

```bash
$ deno bundle index.ts index.bundle.js
Check file:///home/ayo/dev/demo/deno/index.js
Bundle file:///home/ayo/dev/demo/deno/index.js
Emit "index.bundle.js" (7.39KB)

$ deno run --allow-net index.bundle.js
HTTP webserver running. Access it at: http://localhost:8080/
```

Two additional great tools that Deno ships with are the built-in [linter](https://deno.land/manual@v1.14.2/tools/linter) (`deno lint`) and [formatter](https://deno.land/manual@v1.14.2/tools/formatter) (`deno fmt`). In the Node.js ecosystem, linting and formatting code are typically handled with [ESLint](https://eslint.org) and [Prettier](https://prettier.io/), respectively.

When using Deno, you no longer need to install anything or write configuration files to get linting and formatting for JavaScript, TypeScript, and other [supported file formats](https://deno.land/manual@v1.14.2/tools/formatter#code-formatter).

## Unit Testing in Deno

[Support for unit testing](https://deno.land/manual@v1.14.2/testing) is built into Deno for both JavaScript and TypeScript code. When you run `deno test`, it automatically detects any files that end with `_test.ts` or `.test.ts` (also supports other file extensions) and executes any defined tests therein.

To write your first test, create an `index_test.ts` file and populate it with the following code:

```typescript
import { assertEquals } from "https://deno.land/std@0.109.0/testing/asserts.ts";

Deno.test("Multiply two numbers", () => {
  const ans = 2 * 2;
  assertEquals(ans, 4);
});
```

Deno provides the `Deno.test` method for creating a unit test. It takes the name of the test as its first argument. Its second argument is the function executed when the test runs.

There is a second style that takes in an object instead of two arguments. It supports other [properties](https://deno.land/manual@v1.14.2/testing#test-definition-filtering) aside from the test name and function to configure if or how the test should run.

```typescript
Deno.test({
  name: "Multiply two numbers",
  fn() {
    const ans = 2 * 2;
    assertEquals(ans, 4);
  },
});
```

The `assertEquals()` method comes from the `testing` module in the standard library, and it provides a way to easily check the equality of two values.

Go ahead and run the test:

```bash
$ deno test
test Multiply two numbers ... ok (8ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (37ms)
```

## The Deno Language Server

One of the major considerations for choosing a programming language or environment is its integration with editors and IDEs. In Deno 1.6, a built-in language server (`deno lsp`) was added to the runtime to provide features such as:

- autocompletion
- go-to-definition
- linting and formatting
  integration

As well as other language smarts for any editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP). You can learn more about setting up Deno support in your editor in [Deno's online docs](https://deno.land/manual@v1.11.5/getting_started/setup_your_environment#lsp-clients).

## Wrapping up: Should I Choose Deno over Node.js?

In this article, we've considered many aspects of the Deno runtime, and ways in which it's an upgrade over Node.js.

There’s a lot more to say about Deno and its ecosystem, but this should hopefully be a helpful introduction for Node.js developers considering Deno for a new project. The lesser availability of third-party packages for Deno is an obvious aspect where it falls short, as is the fact that it's not as battle-tested as Node.js in the real world due to its young age (Deno 1.0 was released in May 2020).

[Comparing the performance between Node.js and Deno](https://web.archive.org/web/20210421005320/https://mayankchoubey.github.io/Deno-vs-Node-Performance/) shows that they're within the same ballpark in most cases, although there are a few scenarios where Node.js exhibits far superior performance. The measured disparities are bound to improve as Deno becomes more mature.

When deciding between Node.js and Deno, it's also important to keep in mind that some of the benefits that Deno provides can also be brought to Node.js with the help of third-party packages. So if there are only one or two things that you admire about Deno, chances are you'll be able to achieve a similar result in Node.js, though not as seamlessly.

Thanks for reading, and happy coding!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**