Deno, introduced by Node.js creator Ryan Dahl during JSConf 2018, has grown into a credible alternative to Node.js, and the JavaScript and TypeScript communities have continued to track Deno’s progress. Like Node.js, Deno is a system for executing JavaScript code in various environments ( desktop, command-line, server, etc.). It works similarly to Node.js — you write scripts in JavaScript or TypeScript and run them using the deno command line program. However, differences between the two quickly become apparent once you start using Deno.

Secure by design

One of the first differences you might notice between Deno and Node.js is in how scripts are run. To run a server.js script with Node.js,  you would use a command like:

node server.js

The corresponding command in Deno is a bit longer:

deno run --allow-net server.js

There are a couple of items to notice here. First, Deno uses a run subcommand. This is because the Deno binary contains a number of tools, including the script runner; we’ll get back to this later. What’s with that --allow-net argument, though?

That flag, along with several other permissions flags, is part of Deno’s security system. Unlike Node.js, Deno restricts a script’s capabilities by default. Without explicit permission, a Deno script can not open network connections or access the environment, including reading or writing files. By default, a script can’t touch much more than the system console.

Deno’s security system helps ensure that a script run from an untrusted source won’t be able to secretly open a connection back to a data collection server or read files from your local system.

Improved security is great, but having to provide a bunch of --allow-* flags every time you want to run a script would quickly get annoying, particularly if the script needs multiple permissions. To avoid this, Deno provides an install command that will create a thin wrapper in a platform-specific directory ($HOME/.deno/bin on macOS and Linux) for a given script. The wrapper includes the permissions required to run the script, allowing a user to run the script as if it were a standalone command.

The script .deno/bin/server is very simple – it just runs the script from its installed location with the originally specified security flags:

# generated by deno install
exec deno run --allow-net 'file:///Users/jason/server.js' "$@"

No package manager required

Deno is built around ES module import syntax, meaning that imports must be relative or full paths to module files. Unlike Node.js, the Deno loader doesn’t look in a special directory like node_modules, and it doesn’t assume a particular file extension. These work:

import { startServer } from './server.ts';
import { startServer } from '/Users/jason/server.ts';
import * as path from "";

while these do not:

import { startServer } from './server';
import * as path from 'path';

Along with relative and absolute paths, Deno also supports absolute URLs as import targets:

import { startServer } from 'file:///Users/jason/server.js';
import { startServer } from 'https://jason.local/server.js';

This is a very powerful feature — it means that Deno scripts don’t need a package.json or a modules directory (at least not one the developer has to deal directly with), and no npm install step is required to use external packages. If a script needs to use a package available from a repository, it simply imports it:

import * as path from "";

The Deno runtime will download and cache remote modules automatically when a script is run. Locally cached modules will be used by default during subsequent runs, so you don’t need an active network connection every time you want to run a script.


How do remote imports work with Deno’s security system? We saw earlier that Deno doesn’t allow scripts to access the network by default. Since imports will often be from remote URLs, that could mean that many scripts would need the --allow-net permission even if they weren’t explicitly accessing a network (and --allow-write to allow caching). Luckily, imports are a special case — the Deno runtime has implicit permission to download and cache imported modules, so scripts that are just importing remote modules don’t need additional permissions.


Along with downloading packages, package managers also handle package versioning. Deno scripts don’t typically use package managers, so some conventions have been developed to make dealing with external code easier.

The first convention deals with getting specific versions of packages. Since external code is simply imported in Deno without using a package manager, Deno package repositories such as typically include the package version as part of the URL. For example:

import * as path from "";

Of course, this could make version upgrades a mess. Imagine your application has 100 modules, and each of them imports several external modules, with each import statement having its own version number. Not only would updating all of them individually be painful, but if you missed an import you might end up using multiple versions of the same module in your application unintentionally.

The second part of Deno’s standard package management approach directly addresses this problem. Applications can import and re-export all external packages through a “deps” module (deps.ts or deps.js). All application code should import packages from the deps module rather than directly.

// deps.ts
export * as path from "";

// server.js
import { path } from './deps.ts';

Centralizing imports this way makes it easier to see what external modules are used, and it also simplifies the process of updating versions. Deps files work well with Deno tooling such as lock files (used to verify that external modules haven’t changed unexpectedly).

Desktop web environment

Deno tries to use web-standard APIs whenever possible. For example, Deno provides the standard fetch API for making network requests, and it supports the Web Worker API for programs that would benefit from multiple threads. Deno also tries to use web standards in its custom APIs such as its native HTTP server API, which uses standard Request and Response objects. Deno even provides a window object with most of the globals typically available in a browser.

This is in contrast to Node.js, which typically uses its own APIs. For example, Node.js has its own https library for making web requests. This library is lower level than the standard fetch API and is callback-based, so using it is very different from using fetch. Developers frequently reach for a third-party library providing a higher-level API, such as node-fetch.

While web APIs may seem a bit strange to seasoned Node.js developers, using the standard web environment benefits Deno in several ways. Deno gets to take advantage of a decade of API evolution in the JavaScript community, including the use of Promises and async/await rather than callbacks (Node.js is making progress here, but its Promise-based APIs are still a work in progress). Client-side web developers will generally have an easier time writing code for Deno than for Node.js since the basic APIs will be familiar. Deno’s use of web APIs also means that code written for Deno can be more easily ported to a browser environment (and vice-versa), providing more opportunities for code sharing between the front and back ends.

Batteries included

Like Node.js, Deno comes with a standard library covering a wide range of use-cases. Deno’s library is broken into two parts. A runtime library, accessible via a Deno global, provides access to the underlying operating system. The library includes functions to interact with the environment, perform low-level data operations such as copying files, and create HTTP servers.

The Deno developers also provide a standard library of external modules that are audited by the core team and guaranteed to work with Deno. These include node (a Node.js compatibility layer), path (helpers for manipulating paths), and log (a simple logger), along with quite a few more. These modules are not bundled with Deno, but can be imported just like any other module:

import { basename } from "";
const p = basename("./deno/std/path/mod.ts");

Along with code, the Deno binary also provides a number of useful developer tools, all available as deno subcommands. You’ve already seen the script runner (run); some of the others include:

  • test – a test runner that will run tests registered with Deno.test
  • fmt – a code formatter, similar to Prettier
  • compile – a builder that will create a self-contained executable from a script
  • doc – generates documentation from a script
  • info – display a script’s dependency tree
  • lsp – a language server that provides autocompletion, formatting, and error detection features to code editors

Having all of these tools built into the Deno binary is great both for individual developers and for the Deno community. A default set of well-maintained, high-quality core tools may help avoid some of the tooling churn that seems to plague the JavaScript ecosystem.

First class TypeScript

TypeScript lovers rejoice — Deno has built-in support for TypeScript! This support isn’t just bolted on through additional tooling and support modules — TypeScript is a first-class language in Deno. That means that no additional setup is required to run TS scripts. There are no special command line flags or explicit build steps. You don’t even need a tsconfig.json file! Using TypeScript and JavaScript together is also straightforward — JS modules can directly import TS, and TS modules can import JS.

All of the tooling mentioned in the previous section (language server, linter, formatter, etc.) is also TypeScript-aware. They will work on JS or TS files with no special options needed to differentiate between the two.

Although tsconfig files aren’t required, they can be used if necessary. For example, some TS options may need to be set when writing code intended to run in both Deno and in the browser. Unlike the TS compiler, Deno won’t try to find your tsconfig file, so using one requires a command line option

deno run --config ./tsconfig.json server.ts

Simplified deployment

Unlike Node.js, Python, and most other language interpreters, Deno is distributed as a single binary. This binary includes the interpreter, built-in libraries, and all of the built-in tools (linter, formatter, lsp, etc.). This makes the installation and deployment process very straightforward since the only thing a target system needs to run Deno scripts is the deno executable. Deno even provides a compile command that will bundle a script, its dependencies, and the Deno runtime into a standalone executable. It can even cross-compile; for example, you can create a Windows executable on a Mac.

Deno is also very well suited to cloud deployments. The Deno Deploy service makes deployment incredibly easy — just put a Deno script somewhere web-accessible (e.g., GitHub) and deploy it through Deno Deploy’s UI (Deno Deploy also supports direct GitHub integration). The service will acquire the script and make it available through a fully managed global serverless environment. Since Deno Deploy is built on the same infrastructure as desktop Deno, your project doesn’t need a package.json file and there are no explicit build or dependency installation steps.

One thing to keep in mind is that the Deno Deploy environment isn’t the same as desktop Deno. For example, deployed scripts don’t have access to a filesystem, so the Deno global has no readFile method. Deno Deploy provides a deployctl script for running deployable scripts locally; this script is simply a thin wrapper around the standard Deno CLI that provides the same types available in the Deno Deploy environment.

Using Deno

What does using Deno look like in practice? Consider a very simple grep implementation:

// dgrep.ts
import {
} from "";
import { expandGlob } from "";
import {
} from "";

const pattern = new RegExp(Deno.args[0]);
const files = Deno.args.slice(1);

for (const fname of files) {
  for await (const gfile of expandGlob(fname)) {
    const file = await, { read: true });
    const reader = new BufReader(file);
    for await (const line of readLines(reader)) {
      const match = pattern.exec(line);
      if (match) {
        let index = match.index;
          line.slice(0, index)
          + underline(line.slice(index, index + match[0].length))
          + line.slice(index + match[0].length),

This script takes a pattern and one or more files or globs and prints any matching lines, with the first match underlined.

Note that no configuration or installation process is required – Deno handles downloading the script imports automatically. The script is only using imports from the Deno standard library, but it would be no different if the external modules came from any other source.

To make running the script easier to use, we can use the install command:

Now the script can be run like any other local application:

Because Deno handles remote URLs transparently, installing a program from some other source is just as easy, and sharing a script is as easy as making it available over a public URL.


Deno is still new, and while it has a thriving community and a lot of solid packages, its ecosystem isn’t nearly as rich as that of Node.js, and it’s not as well supported by large cloud vendors. That said, Deno’s ease of deployment and focus on simplicity make it an excellent choice for utility scripts, and its support for TypeScript makes it great for larger projects. Deno Deploy provides a solid, Deno-focused target for cloud deployments. The ability to share scripts from anywhere (not just a conformant package repository) makes distributing programs and libraries as easy as pushing them to GitHub or a personal website. 

Check out Awesome Deno for a list of packages, third-party package repositories, and tools to help you get started with Deno!