What is Deno?

Javier Ríos

As their website, also known as dotland says, Deno (/ˈdiːnoʊ/, pronounced dee-no) is a JavaScript, TypeScript, and WebAssembly runtime with secure defaults and a great developer experience.

"node".split("").sort().join("");

Installing Deno

Using Shell (MacOS and Linux)

$ curl -fsSL https://deno.land/x/install/install.sh | sh

Using PowerShell (Windows)

$ irm "https://deno.land/install.ps1" | iex

Build and install from source using Cargo

$ cargo install deno --locked

About Deno

The basics

Deno has a wide variety of features which greatly enhance the Developer Experience, which is key to Deno. These include TypeScript support out of the box for instance. You can avoid the hassle of installing tsc, typescript and all the paraphernalia related to package.json and npm in general.

$ deno run https://deno.land/std@0.176.0/examples/welcome.ts
Welcome to Deno!

We just started and we can see some key Deno concepts already. For instance, we can use URL imports, including those directly pointing to TypeScript sources, the way to exeute code is with the run subcommand, which means there are more subcommands, and that the version is pinned in the URL to the version @0.176.0.

Context

Deno is built on top of Rust, v8 and Tokio. These are the programming language, engine and event loop, respectively. Since Deno is open source, we can play with its internals.

It was created by Ryan Dahl, creator of node too. In his speech 10 things I regret about node, which in fact are 7, he explains node's design caveats. Some include allowing a private company to control the module ecosystem (npm, owned by Microsoft) and not following any standard.

Key features

Security

Deno is secure by default, which means that it doesn't have any dangerous permission granted by default.

The permission system uses flags such as --allow-net and --allow-read, for instance. This allows for a granular permission experience.

If we do not grant a permission to a process, we get this prompt requesting the permission with some extra information.

$ deno run test.ts
   ┌ Deno requests net access to "example.com".
   ├ Requested by `fetch()` API
   ├ Run again with --allow-net to bypass this prompt.
   └ Allow? [y/n] (y = yes, allow; n = no, deny) >

Compatibility

Deno is completely Web compatible. This means that web-standard APIs such as fetch, WebSocket and Blob are perfectly implemented in Deno, and they can be run in the server. This greatly improves the developer experience because one as a developer must not remember runtime-specific APIs.

type ApiResponse<T extends "admin" | "user"> = T extends "admin"
  ? { salary: number }
  : { tier: "free" | "premium" };

const res = await fetch("https://api.example.dev/admin/whoami", {
  headers: {
    "Authorization": `Bearer ${Deno.env.get("AUTH_TOKEN")}`,
  },
});

const raw = new TextDecoder().decode(await res.arrayBuffer());

const data: ApiResponse<"admin"> = JSON.parse(raw);

console.log(data.salary);

In this code snippet there is a trivial conditional type ApiResponse, and also an API call to get some data. This code (once transpiled to JavaScript) will run perfectly on the client. To run it on the server this is not necessary. Web APIs such as fetch and TextDecoder are used.

New standards

Deno also supports top-level await without tedious configuration. This avoids the use of legacy IIFEs (Immediately Invoked Function Expressions), which clutter the code and add unnecessary boilerplate. This also means that other modules, when importing, wait for the whole module to be evaluated.

// Without top-level await support
(async function () {
  const data = await doSomething({ admin: false });
  console.log(data);
})();

// With top-level await support
const data = await doSomething({ admin: false });
console.log(data);

Deno vs Node

Node setup

Installing the required packages

Let's create a TypeScript project with node:

# Create default package.json
$ npm init -y

# Install TypeScript as a dev dependency
$ npm install -D typescript

# Install types for node
$ npm install -D @types/node

# Create tsconfig.json file with defaults
$ npx tsc --init

# Edit the tsconfig.json to use newer options
$ nvim tsconfig.json

Setting up the tsconfig.json

By default the generated tsconfig.json by tsc uses old presets, we must update them. I suggest you look at all the available options and choose your prefered ones.

{
  "compilerOptions": {
    "target": "es2022", // latest target
    "module": "es2022", // use import/export instead of require
    "lib": ["es6", "dom"], // enable `es6` + `dom` APIs
    "allowJs": true, // import .js files too
    "outDir": "dist", // place transpiled code in `dist/`
    "rootDir": "src", // get our sources from `src/`
    "strict": true, // enable strict mode
    "noImplicitAny": true, // prohibit `any`
    "esModuleInterop": true // enable cjs imports
  }
}

Setting up a file watcher

Due to the nature of TypeScript, it must be transpiled to JavaScript before running it. It is extremely tedious to run npx tsc every time, so we will do that automatically. This will automatically transpile and run on every file change.

# Install file watcher + transpiler
$ npm i -D nodemon ts-node

Now we need to edit package.json to add the script.

{
  "scripts": {
    "watch": "nodemon --watch \"src/**\" --ext \"ts,json\" --ignore \"dist/**\" --exec \"ts-node src/index.ts\""
  }
}

And lastly execute the script we just created.

# Start the watcher
$ npm run watch

Time to write some code

$ mkdir src
$ nvim src/index.ts
console.log("Hello from Node!");

Now the console should yield:

Hello from Node!

If you are not familiar with setting up TypeScript, this setup can become unpleasant and quite tedious, just to get your code transpiling correctly.

Something to consider

Some new tsconfig.json setups require some trickery and tweaks to get everything running smoothly. Take for instance this code:

import doSomething from "../../utils";

console.log(await doSomething({ admin: false }));

The transpiled code is the same, but node cannot resolve the import!

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '...' imported from '...'

To fix this, we have to import out TypeScript sources from .js files. Not ideal in my opinion.

import doSomething from "../../utils.js";

console.log(await doSomething({ admin: false }));

Deno setup

Running TypeScript

Let's create a main.ts file and add some TypeScript:

console.log("Hello from Deno!");

And done. Is this a world record?

$ deno run main.ts
Hello from Deno!

Setting up the watcher

Let's create a Deno configuration file, deno.jsonc. The .jsonc extension stands for JSON with comments.

Deno provides a builtin watcher, under the --watch flag.

{
  // user-defined scripts
  "tasks": {
    "dev": "deno run --watch main.ts"
  }
}

Let's run the watcher:

$ deno task dev

Deno's toolchain

These are some of the most useful deno subcommands. You can check the manual directly here, it is worth checking out!

Init

Even though Deno doesn't need more than a file to start, this subcommand scaffolds a project with testing, benchmarks and file watching.

$ deno init
✅ Project initialized

Run these commands to get started

  # Run the program
  deno run main.ts

  # Run the program and watch for file changes
  deno task dev

  # Run the tests
  deno test

  # Run the benchmarks
  deno bench

Bench

Deno has a built-in benchmark runner that you can use to check the performance of JavaScript or TypeScript code.

We first set up benchmarks with the Deno.bench API:

import { add } from "./main.ts";

Deno.bench(function addBig() {
  add(2 ** 32, 2 ** 32);
});

And then we get the results:

$ deno bench
Check file:///Users/javii/Code/demo/main_bench.ts
cpu: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz
runtime: deno 1.30.2 (x86_64-apple-darwin)

file:///Users/javii/Code/demo/main_bench.ts
benchmark      time (avg)             (min … max)       p75       p99      p995
------------------------------------------------- -----------------------------
addBig       6.64 ns/iter    (5.62 ns … 75.78 ns)   6.83 ns  14.11 ns  14.93 ns

Bundle

This will output a single JavaScript file for Deno's consumption, which includes all dependencies of the specified input.

$ deno bundle https://deno.land/std@0.173.0/examples/colors.ts colors.bundle.js
Bundle https://deno.land/std@0.173.0/examples/colors.ts
Download https://deno.land/std@0.173.0/examples/colors.ts
Download https://deno.land/std@0.173.0/fmt/colors.ts
Emit "colors.bundle.js" (9.83KB)

After that, we can consume the JavaScript from the generated self-contained module.

import { green } from "./colors.bundle.js";

console.log(green("I am green!"));

Compile

This subcommand generates a self-contained executable. It is imperative for flags to be set at compile time, or else they won't work. You can also specify the target arch with the --arch flag.

$ deno compile --allow-read --allow-net https://deno.land/std/http/file_server.ts

Then we just run the executable. This is a great way to distribute code.

$ ./file_server -help

Doc

Deno is also capable of generating documentation. It transforms the JSDoc present in source files to build it up. Only exported members have documentation generated.

/**
 * 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;
}

This would be the example output for the add function:

$ deno doc add.ts
function add(x: number, y: number): number
  Adds x and y. @param {number} x @param {number} y @returns {number} Sum of x and y

Fmt

Deno ships with an opinionated formatter, which used to be even more opinionated. It is as easy as running the subcommand, zero configuration is needed. The benefit of this formatter is that it maintains a consistent style across codebases. To ignore some code, add the deno-fmt-ignore comment above. This formatter supports .ts, .tsx, .js, .jsx, .md, .json and .jsonc.

$ deno fmt
/Users/javii/Code/demo/main.ts
Checked 4 files

Configuration can be set under the fmt field of the deno.jsonc config file. This is an example configuration with all possible options:

{
  "fmt": {
    "files": {
      "include": ["src/"],
      "exclude": ["src/testdata/"]
    },
    "options": {
      "useTabs": true,
      "lineWidth": 80,
      "indentWidth": 4,
      "singleQuote": true,
      "proseWrap": "preserve"
    }
  }
}

Lint

Deno also ships with a JavaScript and TypeScript linter. All the available rules are listed here.

$ deno lint
(no-explicit-any) `any` type is not allowed
export const fn = (b: any) => b + 1;
                      ^^^
    at /Users/javii/Code/demo/main.ts:1:23

    hint: Use a specific type other than `any`
    help: for further information visit https://lint.deno.land/#no-explicit-any

Found 1 problem
Checked 3 files

Task

We have already used this subcommand, and it provides a way to run custom commands. First we need to define them in a tasks property in the deno.jsonc configuration file.

{
  "tasks": {
    "data": "deno task collect && deno task analyze",
    "collect": "deno run --allow-read=. --allow-write=. scripts/collect.js",
    "analyze": "deno run --allow-read=. scripts/analyze.js"
  }
}

Now we can run the defined task as follows:

$ deno task collect

Test

Deno also ships with a builtin test runner to make unit tests. This command has two steps, similar to the bench subcommand. We first register the test.

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

Deno.test("url test", () => {
  const url = new URL("./foo.js", "https://deno.land/");
  assertEquals(url.href, "https://deno.land/foo.js");
});

After that, we can generate the report:

$ deno test url_test.ts
running 1 test from file:///dev/url_test.js
test url test ... ok (2ms)

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

Extras

Deno still has two aces up its sleeve. A next-gen web framework, and a great place to deploy your Deno code!

Fresh

Fresh is a next-gen framework. It does just-in-time rendering on the edge! This means that the server serving the data is as closest as possible to the localization of the request. This blog post explains it in depth, and is worth a read.

The basics

Besides that, it uses island-based client hydration. It is a fancy way of saying that only certain parts of your website have JavaScript shipped to the client, in order to minify waiting time.

Another important characteristic is that it uses file-system routing à la Next.js. File structure directly resembles the available endpoints!

It also gives you the option of using twind for the styling, which is recommended. It uses preact instead of react as well.

Starting a new fresh project is as easy as excuting two commands:

# Scaffold the project and install dependencies
$ deno run -A -r https://fresh.deno.dev my-fresh-app
# Start the server on http://localhost:8000/
deno task start

Project structure

This is the file structure created:

$ tree
.
├── README.md
├── components
│   └── Button.tsx
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│   └── Counter.tsx
├── main.ts
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
├── static
│   ├── favicon.ico
│   └── logo.svg
└── twind.config.ts

Key parts

The important folders are components/, islands/ and routes/.

Components

The components/ folder is where normal components are placed. These components have no JavaScript. If they did have it, it will get stripped out. This is an example of a button. Extra functionality is included under the $fresh module.

import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";

export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
  return (
    <button
      {...props}
      disabled={!IS_BROWSER || props.disabled}
      class="px-2 py-1 border(gray-100 2) hover:bg-gray-200"
    />
  );
}

Islands

The islands/ folder is where components with JavaScript get placed. These components ship JavaScript to the client, in order to make the website interactive. Due to this, they are heavier in size. Components with JavaScript must be inside this directory.

import { useState } from "preact/hooks";
import { Button } from "../components/Button.tsx";

export default function Counter(props: { start: number }) {
  const [count, setCount] = useState(props.start);
  return (
    <div class="flex gap-2 w-full">
      <p class="flex-grow-1 font-bold text-xl">{count}</p>
      <Button onClick={() => setCount(count - 1)}>-1</Button>
      <Button onClick={() => setCount(count + 1)}>+1</Button>
    </div>
  );
}

Routes

The index.tsx file is the main entry point. It must export a component.

import { Counter } from "../islands/Countex.tsx";

export default function Home() {
  return (
    <div>
      <Counter />
    </div>
  );
}

Deploy

Deploy is a distributed system that allows you to run JavaScript, TypeScript and WebAssembly close to users, at the edge, worldwide.

Deploy also provides the latest and greatest in web technologies in a globally scalable way.

It also supports CI/CD thanks to deployctl.

job:
  permissions:
    id-token: write
    contents: read
  steps:
    - name: Deploy to Deno Deploy
      uses: denoland/deployctl@v1
      with:
        project: awesome-fresh-site
        entrypoint: main.ts

Wrapping up

Deno is a great tool, and it is in a really early phase yet. The best is yet to come!