Deno logoDeno

A list of every web API in Deno

Luca Casonato


Have you ever wondered how web compatible Deno is? Probably not, but I did today. To answer that question, I am writing this blog post: I'll list and explain every single web API implemented in Deno. Get yourself something to drink, because this is going to take a while to go through.

Before we get into it though, I just want to set some ground rules:

  1. I am not including any JS language features. Only web platform features.
  2. I will include the few features that are still marked as --unstable, but will mention that they are not yet stable.
  3. I am not including any features that are not a concrete API. For example JSON modules are implemented in Deno, but are more of an abstract concept rather than a concrete API and thus are not included in this list.

Table of Contents

atob and btoa

These two functions are used to encode and decode base64 strings. They're old - Firefox 1, released on the 9th of November 2004, already had support.

atob("SGVsbG8gV29ybGQ="); // "Hello World"
btoa("Hello World"); // "SGVsbG8gV29ybGQ="

setTimeout and clearTimeout

setTimeout is another super old web feature. It's used to schedule a function to be called after a certain amount of time has passed. It returns a numeric ID that can be used to cancel the timeout with clearTimeout.

Beware that Deno implements this API just like in the browser: returning a number. Node.js also implements this API, but with a non-standard behavior where it returns an object instead of a number.

setTimeout(() => {
  console.log("This prints after 1 second.");
}, 1000);

const timerId = setTimeout(() => {
  console.log("This doesn't print at all.");
}, 500);
clearTimeout(timerId);

console.log("This prints immediately.");

setInterval and clearInterval

setInterval and clearInterval are very similar to setTimeout and clearTimeout. The difference is that setInterval calls the callback function every X milliseconds, rather than only once after X milliseconds.

Same disclaimer as with setTimeout applies here: Deno implements this API just like the browser, while Node has a non standard object as the return value.

crypto

Deno implements the Web Cryptography API completely. This API can be used to do various low and high level cryptographic operations. For example you can:

  • generate a random UUID with crypto.randomUUID()
  • generate a Uint8Array filled with random bytes with crypto.getRandomValues()
  • sign and verify data with crypto.subtle.sign() and crypto.subtle.verify() with RSA, ECDSA, and many other algorithms.
  • generate hashes with crypto.subtle.digest().
  • encrypt and decrypt data with crypto.subtle.encrypt() and crypto.subtle.decrypt().
  • generate RSA, ECDSA, HMAC, or AES keys with crypto.subtle.generateKey()

Deno's implementation of the Web Cryptography API was completed just a few weeks ago, but the wait was totally worth it. Deno passes more web platform tests than both Chrome and Firefox. Only Safari has us slightly beat:

Engine % of tests passing
Safari 99.8%
Deno 98.1%
Chrome/Edge 94.5%
Firefox 93.4%

You can see the current data for yourself on wpt.fyi.

const uuid = crypto.randomUUID();
const bytes = await crypto.getRandomValues(new Uint8Array(16));
const digest = await crypto.subtle.digest("SHA-256", bytes);

fetch, Request, Response, and Headers

Deno implements the maybe most popular modern web API: fetch. It's used to make HTTP requests. Usage is super simple: the first argument is the URL you want to request, and the second argument is an optional object with options, like method, headers, or body.

The fetch API in Deno is implemented natively in the runtime, backed by the blazing fast hyper HTTP implementation in Rust. It supports HTTP/1.1 and HTTP/2 servers, full duplex streaming, native gzip/deflate/brotli decoding, and is highly compatible with the web platform.

Just like all of our other web APIs, our implementation of fetch is tested using the same web platform tests test-suite as all the browsers. This makes sure that Deno's implementation is compatible with the web platform. To our knowledge, we are the only server side runtime that tests fetch against the canonical web platform tests right now.

Deno also implements all of the objects surrounding fetch: Request, Response, and Headers. These objects represent a HTTP request, a HTTP response, and a list of HTTP headers respectively. Because Deno is a web-first runtime, our HTTP server uses these same objects to represent the request and response. That makes proxying requests super easy.

const resp = await fetch("https://whats-my-ip.deno.dev/");
const text = await resp.text();
console.log(text);

Blob and File

Blob and File both represent binary data. The big difference between them and Uint8Array is that they can store their data in memory or on disk, thus making them ideal for large binary blobs. Because of the possibility of storing data on disk, the data backed by Blob and File is only available asynchronously.

Data can be retrieved using the .arrayBuffer(), .text(), and .stream() methods, or the FileReader API.

const blob = new Blob(["Hello World"]);
const text = await blob.text();
console.log(text);

const file = new File(["Hello World"], "hello.txt");
console.log(file.name);
console.log(file.size);
const bytes = await file.arrayBuffer();
console.log(new Uint8Array(bytes));

TextEncoder and TextDecoder

Sometimes you need to encode or decode strings into or from a binary representation. Deno provides the TextEncoder and TextDecoder APIs for this. They can be used to encode strings into a Uint8Array, and decode Uint8Arrays into strings. All of the text encodings that are available in the browser are also available in Deno.

const encoder = new TextEncoder();
const bytes = encoder.encode("Hello World");
console.log(bytes);

const decoder = new TextDecoder();
const text = decoder.decode(bytes);
console.log(text);

FormData

When interacting with HTTP APIs, sometimes you need to send data as multipart/form-data. This can be done using the FormData API. It is a JavaScript structure that lets you easily append key-value pairs or files to a form to be sent as multipart data.

Because Deno also uses the same Request and Response objects from fetch for the native HTTP server, FormData can be used to easily decode multipart form data from incoming requests by calling await request.formData(). Neat huh?

const formData = new FormData();
formData.append("name", "Deno");
formData.append("age", "3");
formData.append("file", new File(["Hello World"], "hello.txt"));

const resp = await fetch("https://httpbin.org/post", {
  method: "POST",
  body: formData,
});

performance

The performance allows for accurate time measurement, for example using performance.now(). It can also be used to directly measure the time spent on some operation using performance.mark() and performance.measure().

const start = performance.now();
await fetch("https://httpbin.org/delay/1");
const end = performance.now();
console.log("Took", end - start, "milliseconds");

structuredClone

The structuredClone API is used to deep clone objects. It is a very new API, but is already shipping in all major browsers.

Compared to shallow cloning, deep cloning doesn't just copy the shape of the outermost object, but also the shape of all nested objects and arrays. It can also clone objects with circular references.

const obj = {
  foo: "bar",
  baz: {
    qux: "quux",
  },
};
const clone = structuredClone(obj);
console.log(obj === clone); // false
console.log(obj.baz === clone.baz); // false

obj.baz.qux = "quuz";
console.log(obj.baz.qux); // quuz
console.log(clone.baz.qux); // quux

URL and URLSearchParams

URLs are integral to anything having to do with the web. Deno implements the URL and URLSearchParams APIs as they are specified by the WHATWG. This means that our URL and query string parsing is exactly the same as the browser. This can prevent security issues around one system parsing a URL slightly differently to another system.

const url = new URL("https://example.com/foo");
console.log(url.href); // https://example.com/foo
console.log(url.hostname); // example.com
console.log(url.searchParams.get("name")); // undefined
url.searchParams.append("name", "bar");
console.log(url.href); // https://example.com/foo?name=bar

console

console may be the most useful web API out there. Printf debugging anyone? You probably know about console.log() already, so here are some other cool features of console that Deno implements that you may not know about:

%c marker can be used to add color to logging output using CSS. Deno understands this natively - no need to remember ANSI escape codes anymore to do colored logging:

console.log("%cHello, world!", "color: red");
console.log("%cHello, %cworld!", "font-weight: bold", "font-style: italic");

Another cool console feature we implement is the console.time API. It makes it super simple to figure out how long some operation took:

console.time("foo");
await new Promise((resolve) => setTimeout(resolve, 1000));
console.timeEnd("foo");

Worker

JavaScript execution is always single threaded - the language has no built in concurrency primitives. To support workloads that require multiple threads, the web has the concept of Worker. Workers are additional JavaScript execution contexts that run on a separate thread, completely isolated from the main thread.

Deno implements workers natively, just like in the browser. Because the APIs are identical you can use libraries that are written for the browser in Deno without any changes. An example of such a library is comlink - it makes using workers super simple by abstracting away the details of cross worker communication.

// main.js
import * as Comlink from "https://cdn.skypack.dev/comlink@4.3.1?dts";

const url = new URL("./worker.js", import.meta.url);
const worker = new Worker(url, { type: "module" });

const obj = Comlink.wrap(worker);

console.log(`Counter: ${await obj.counter}`);
await obj.inc();
console.log(`Counter: ${await obj.counter}`);

worker.terminate();
// worker.js
import * as Comlink from "https://cdn.skypack.dev/comlink@4.3.1?dts";

const obj = {
  counter: 0,
  inc() {
    this.counter++;
  },
};

Comlink.expose(obj);

Event and EventTarget

In the browser events and event targets are the basis of all interactive experiences. They allow you to register a callback to be called when some event occurs. FileReader is an EventTarget for example: it emit events when a chunks of the file are read, or when the file is completely read, or when an error occurs.

Modern APIs often don't use events and callbacks anymore, instead using Promises. These have much better usability and readability because of async/await. Nonetheless Event and EventTarget are still used by many APIs, so Deno implements them just like browsers.

const target = new EventTarget();
target.addEventListener("foo", (event) => {
  console.log(event);
});
target.dispatchEvent(new Event("foo"));

WebSocket

Just like how Deno implements fetch do to HTTP requests just like in the browser, Deno also implements the WebSocket API. WebSockets are great way to do bi-directional communication between a client and server.

Because the WebSocket protocol works the same on both ends of the connection (client and server), Deno also uses this same API for server-side (incoming) WebSocket connections on the native HTTP server.

const ws = new WebSocket("ws://localhost:4500");
ws.onopen = () => {
  ws.send("Hello, world!");
};
ws.onmessage = (event) => {
  console.log(event.data);
  ws.close();
};

ReadableStream and WritableStream

Streaming is a critical part of modern web applications. It is a necessity when working with data that is larger than can fit into available memory at once. It is also a great way to reduce the TTFB when working with large files.

Deno supports the same streams API as browsers. This is very powerful, because it allows Deno users to make use of stream transformers written for the browser. It also lets web developers use stream transforms written for Deno on their website. For example, the std/streams module from Deno's standard library can be used from both Deno and the browser to do things like split streams of text at newlines.

Readable streams are the most common type of stream, and are used when you want to read data from a source. For example, you might want to stream the response body of a HTTP request, or the contents of a file on disk.

Writable streams are the inverse - they are used when you want to write data to a destination in chunks. One such case is the sending side of a WebSocketStream (more on this below).

const body = new ReadableStream({
  start(controller) {
    controller.enqueue(new Uint8Array([1, 2, 3]));
    controller.enqueue(new Uint8Array([4, 5, 6]));
    controller.close();
  },
});

const resp = await fetch("https://httpbin.org/anything", { body });
for await (const chunk of resp.body) {
  console.log(chunk);
}

TransformStream

Next to the low level stream primitives ReadableStream and WritableStream, the web also has a general purpose API to transform streaming data called TransformStream. They have a readable and a writable end, and a "transformer" function. For each chunk that is written to the writable end, the transformer function is called. It can then enqueue transformed chunks to the readable end for the consumer to read.

const input = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello, ");
    controller.enqueue("world!");
    controller.close();
  },
});

const transformer = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  },
});

const output = input.pipeThrough(transformer);

for await (const chunk of output) {
  console.log(chunk);
}

WebSocketStream

As mentioned earlier, Deno implements the WebSocket API that is built on top of EventTarget. This can sometimes be pretty painful to use. As an alternative you can use the WebSocketStream API which is based on streams and promises. It is a lot easier to use when you are using async/await.

const wss = new WebSocketStream("wss://example.com");
const { writable, readable } = await wss.connection;

const writer = writable.getWriter();
await writer.write("Hello server!");

for await (const message of readable) {
  console.log("new message:", message);
}

NOTE: this API is still experimental and may change in the future. You need to use the --unstable flag to use it.

TextEncoderStream and TextDecoderStream

The TextEncoderStream and TextDecoderStream are transform streams that can encode and decode strings to and from bytes in chunks. Unlike TextEncoder and TextDecoder are usually used for fully synchronous operations, TextEncoderStream and TextDecoderStream are fully streaming.

This API makes it super easy to convert the response stream from a fetch call to string chunks:

const response = await fetch("https://example.com");
const body = response.body.pipeThrough(new TextDecoderStream());
for await (const chunk of body) {
  console.log(chunk);
}

CompressionStream and DecompressionStream

Another operation that often needs to performed on streaming data is compression and decompression. You can use CompressionStream to gzip compress some data before uploading to a storage bucket for example. When downloading that data again you can decompress it with DecompressionStream.

Just like Chrome, Deno supports gzip and deflate compression. Support for brotli compression is coming soon.

const response = await fetch("https://example.com");
const body = response.body.pipeThrough(new CompressionStream("gzip"));
const file = await Deno.create("./file.gz");
for await (const chunk of body) {
  await file.write(chunk);
}

CompressionStream is available in the latest Deno canary build, but is not yet available in the 1.18 stable build. The first stable build it will be available in is Deno 1.19.

URLPattern

URLPattern is a new web API to match URLs against a path-to-regexp style pattern. It is super useful for creating routing systems for HTTP servers for example. The API is available in Chrome and Deno, and Firefox is interested in implementing it.

const pattern = new URLPattern({ pathname: "/hello/:name" });
const match = pattern.exec("https://example.com/hello/Deno");
console.log(match.pathname.groups);

alert, confirm and prompt

CLI applications often need to interact with the user to some extent. Deno implements the simple dialog APIs alert, confirm and prompt from the web platform for this purpose. You can alert a user with a message and wait for acknowledgement, ask the user to confirm a yes/no question, or prompt the user for a string response.

let name;
do {
  name = prompt("What is your name?");
} while (!confirm(`Is your name ${name}?`));
alert(`Hello, ${name}!`);

localStorage and sessionStorage

When writing CLI applications it is also often useful to persist a little bit of state across runs (for example an API access token). This can be done trivially with the localStorage API. It is a persistent key-value store for each Deno application.

const count = parseInt(localStorage.getItem("count") || "0");
console.log(`You have started the application ${count} times previously.`);
localStorage.setItem("count", count + 1);

The window.navigator object contains some useful information about the current system, like the number of available CPU cores / threads for example. navigator.hardwareConcurrency contains the number of logical CPU cores.

console.log("This system has", navigator.hardwareConcurrency, "CPU cores.");

WebGPU

All of the previous APIs have either been related to the CPU, or some I/O primitive. Deno also supports accessing the GPU through the WebGPU API. This low-level API can be thought of as "vulkan for the web". It allows for efficient low level access to the GPU to perform rendering and compute tasks.

Deno bases it's WebGPU implementation of the same Rust library that Firefox uses: wgpu.

const adapter = await navigator.gpu.requestAdapter();
console.log("GPU adapter:", adapter.name);

// this blog post is already long enough, I won't go into
// low level GPU programming here :-)

NOTE: this API is still experimental and may change in the future. You need to use the --unstable flag to use it.

MessageChannel

One lesser known web API is MessageChannel. It is a pair of streams that can be used to communicate between two workers. Each channel has two "ports", each of which can be either kept in the current worker or transferred to another worker.

This allows for really complex communication channels between workers, without requiring a single centralized worker to act as a "message proxy".

const channel = new MessageChannel();
const { port1, port2 } = channel;
port1.onmessage = (event) => {
  console.log(event.data);
  port1.close();
};
port2.postMessage("Hello, world!");
port2.close();

BroadcastChannel

BroadcastChannel is similar to MessageChannel in that it is a channel to communicate between workers. However, unlike MessageChannel, it is a 1 to many channel rather than a 1 to 1 channel.

const channel = new BroadcastChannel("my-channel");
channel.onmessage = (event) => {
  console.log(event.data);
};
channel.postMessage("Hello, world!");

NOTE: this API is still experimental and may change in the future. You need to use the --unstable flag to use it.