Back to Scaling Node.js Applications guides

Exploring Deno: Is It Time to Ditch Node.js?

Stanley Ulili
Updated on November 16, 2024

For years, Node.js dominated as the go-to runtime for executing JavaScript outside the browser. But in 2020, the introduction of Deno disrupted this landscape, offering a fresh alternative that directly addresses Node.js's shortcomings.

Deno comes with native TypeScript support, a secure permissions-based sandbox, and an integrated toolkit that includes a code formatter, linter, test runner, and build tool for self-contained executables.

Its innovative approach to dependency management eliminates the need for package.json and node_modules, yet it retains full compatibility with the Node.js ecosystem to allow you choose the approach that works best for you.

This guide explores Deno's features and compares them with Node.js to help you decide if switching is right for you.

Let's get started!

Prerequisites

If you want to experiment with the features described in this article, refer to the Deno installation instructions.

You can verify your installation with:

 
deno --version

This should produce an output similar to:

Output
deno 2.0.0 (stable, release, x86_64-unknown-linux-gnu)
v8 12.9.202.13-rusty
typescript 5.6.2

Understanding Deno

Deno is a modern alternative to Node.js, a widely-used runtime for building server-side applications. Both were created by Ryan Dahl, who developed Deno to address limitations he identified in Node.js.

In a 2018 presentation, Dahl outlined these shortcomings, including:

  • The absence of a unified approach to asynchronous programming with Promises,
  • Missed opportunities to enhance security,
  • The complexities of the build system (GYP),
  • And challenges tied to the module system, such as reliance on package.json, node_modules, and implicit file extensions.

Deno tackles these issues by offering a secure, modern, and streamlined runtime. It enforces security through an explicit permissions model for file system and network access, simplifying the module system by adopting URL-based imports with required file extensions.

Native TypeScript support is built-in to eliminate the need for additional tools or configurations. Deno also adheres to web standards, facilitates the creation of standalone executables, and is built on V8 and Rust for performance and reliability.

In Deno 2, compatibility with the Node.js ecosystem was significantly enhanced, making it a compelling choice for developers seeking a more secure and modern development experience without sacrificing access to existing tools and libraries.

To better understand its potential and how it stacks up against Node.js, we'll dive deeper into Deno's key features and compare them with those of its predecessor.

A modern approach to tooling

Deno provides a comprehensive suite of built-in tools, eliminating the need for third-party dependencies often required in other runtimes like Node.js. This seamless integration simplifies development, minimizes dependency management, aligns Deno with the integrated approach to tooling found in modern languages like Go and Rust.

Diagram illustrating Deno's tooling ecosystem

Let's look at each of these in turn:

REPL (Read-Eval-Print Loop)

Deno includes a robust REPL for quick experimentation. Unlike Node.js' REPL, Deno's counterpart supports TypeScript natively, allowing you to write and test TypeScript code directly without prior compilation:

 
Deno 2.0.6
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.
> console.log(((name: string) => `Hello, ${name}!`)("Alice"));
Hello, Alice! undefined >

File watcher

Screenshot of Deno's file watcher

Deno's file watcher automatically restarts your application when it detects changes in your code.

 
deno run --watch app.js
Output
Watcher Process started.
HTTP server running. Access it at: http://localhost:8080/
Listening on http://0.0.0.0:4000/

It also supports Hot Module Replacement (HMR) which enables application updates without a without a complete restart:

 
deno run --watch-hmr app.js

Node.js introduced a watch feature of its own in v18.11.0 which works in a similar manner, but it currently lacks hot module replacement functionality.

Linter and formatter

One of the features that sets Deno apart from Node.js is its built-in linter and formatter which makes it possible to enforce code quality standards without resorting to third-party dependencies.

You can access them with the following commands:

 
deno lint
 
deno fmt

Node.js doesn't offer a built-in linter or formatter, so you'll need to use a tool like ESLint, Prettier, or BiomeJS.

Test runner

Writing tests for existing code is one of the most common tasks in software development, and Deno simplifies this process with its built-in test runner:

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

function add(a, b) {
  return a + b;
}

Deno.test("Add two numbers", () => {
  const result = add(2, 2);
  assertEquals(result, 4);
});

You can run tests with:

 
deno test

Screenshot of test output

In Deno 2, you can run test code examples directly within JSDoc comments or Markdown files using the --doc option which is useful for ensuring that your documentation's code snippets stay accurate and up to date.

app.js
/**
 * Adds two numbers together.
 *
 * # Examples
 *
 * ```js
 * const sum = add(1, 2);
 * console.assert(sum === 3, 'Expected 3, but got ' + sum);
 * ```
 *
 * @param {number} a - The first number to add.
 * @param {number} b - The second number to add.
 * @returns {number} The sum of the two numbers.
 */
export function add(a, b) {
  return a + b;
}

You can now test the code examples directly from the documentation with:

 
deno test --doc app.js

Compile command for creating executables

Deno simplifies the process of creating standalone executables from your TypeScript or JavaScript code. Unlike Node.js, which requires a more intricate approach to building single executable applications, Deno enables you to generate an executable with a single command:

 
deno compile app.js
Output
Check file:///home/dev/betterstack/demo/prometheus-metrics/app.js
Compile file:///home/dev/betterstack/demo/prometheus-metrics/app.js to app

You can then run the compiled binary like any other executable:

 
./app

Deno's ecosystem is further enhanced with tools such as:

  • deno bench: For creating and running benchmarks to measure and optimize code performance.
  • deno jupyter: A Deno kernel for Jupyter notebooks, enabling interactive TypeScript development in a notebook environment.

These integrated tools make Deno a comprehensive and efficient platform for JavaScript and TypeScript development, minimizing the need for external dependencies.

Permissions in Deno

One of Deno's most notable features is its secure-by-default approach. Unlike Node.js, where programs can access networks, file systems, and other sensitive resources without restrictions, Deno prevents access to these resources by default. To perform actions like network requests, you must explicitly grant permission, which significantly enhances the security of your applications.

Screenshot of the Deno Runtime

For example, consider the following code that fetches a to-do item from an API:

app.js
async function fetchTodo(id) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
  if (!response.ok) {
    throw new Error(`API request failed with status: ${response.status}`);
  }
  return await response.json();
}

fetchTodo(1).then((todo) => {
  console.log("Fetched To-Do Item:", todo);
});

When you run this program, Deno detects that it requires network access:

Output
┌ ⚠️  Deno requests net access to "jsonplaceholder.typicode.com:443".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

If you deny the request by entering n, the program will fail with a PermissionDenied error, indicating that network access was blocked. To allow network access, you can rerun the program with the --allow-net flag:

 
deno run --allow-net app.js

This will enable the program to fetch the data successfully:

Output
Fetched To-Do Item: { userId: 1, id: 1, title: "delectus aut autem", completed: false }

For more granular control, you can restrict network access to specific domains or IP addresses:

 
deno run --allow-net='jsonplaceholder.typicode.com' app.js

This security model not only improves the safety of your applications but also allows for more precise permission management. Beyond network access, Deno offers other permissions, such as:

  • --allow-read: Grants read access to the file system.
  • --allow-write: Grants write access to the file system.
  • --allow-env: Grants access to environment variables.
  • --allow-run: Allows spawning subprocesses.
  • --allow-ffi: Allows loading dynamic libraries (Foreign Function Interface).

These options ensure that your Deno applications operate securely while having the necessary access to resources.

Configuring Deno

Deno offers a flexible configuration system that lets you customize various aspects of the runtime environment, including the TypeScript compiler, formatter, linter, and more. While not required for execution, a configuration file allows you to tailor Deno's behavior to suit your project's specific needs.

As mentioned earlier, Deno supports configuration files with .json and .jsonc extensions. Since version 1.18, Deno automatically detects these files in your working or parent directories.

Here's an example of a typical deno.json configuration file:

deno.json
{
  "imports": {
    "std/assert": "jsr:@std/assert@^1.0.0"
  },
  "tasks": {
    "dev": "deno run --watch app.ts"
  }
}

This configuration highlights two key features: the imports and tasks fields. The imports field, functioning as an import map, lets you create aliases for modules, simplifying module management. The tasks field, similar to the scripts field in Node.js's package.json, defines custom scripts that can be run with the deno task command, enabling automation of tasks like starting a server or running tests directly within the Deno runtime.

To run a task defined in the configuration, you use:

 
deno task dev

In this case, the dev task would start your application in watch mode, automatically reloading the server on file changes.

Beyond these basics, the configuration file offers other options that further enhance your control over Deno's behavior:

  • compilerOptions: Customize TypeScript compiler settings to match your project's needs, such as enabling strict type checking or targeting specific JavaScript versions.
  • lint: Configure Deno's built-in linter, allowing you to enforce coding standards and best practices across your codebase.
  • fmt: Set up the code formatter to ensure consistent styling, making your code easier to read and maintain.
  • test: Define how tests are run, including options for test filtering and setup configurations, to streamline your testing process.

First-class TypeScript support

Screenshot of the Deno execution workflow

Deno offers first-class support for TypeScript with zero configuration, allowing you to write type-safe programs without complex build systems.

The compiled JavaScript is subsequently cached, so that it doesn't need to be recompiled for subsequent executions.

For example:

app.ts
interface User {
  name: string;
}

function welcomeUser(user: User): string {
  return `Hello, ${user.name}!`;
}

console.log(welcomeUser({ name: "Alice" }));

You can run this TypeScript file as easily as a JavaScript file:

 
deno run app.ts

This will output:

Output
Hello, Alice!

Here, no separate compilation step is necessary. The caching mechanism ensures that your TypeScript code is only compiled once, which speeds up repeated executions.

If you're curious about the details, including the cache structure, you can inspect this using the deno info command, which reveals where Deno stores the transformed JavaScript and related metadata.

Dependency management in Deno

Deno introduces a unique approach to dependency management by adopting a web-centric model emphasizing simplicity, security, and adherence to standards.

Instead of using traditional package managers like npm or Yarn, Deno employs a URL-based import system. This is similar to how web browsers handle module imports, offering several benefits:

  • Transparency, where developers can easily trace the source of each dependency,
  • Versioning, by pinning specific versions directly in the import URL,
  • Decentralization, by allowing modules to be hosted anywhere, not just on a central registry.

For example:

 
import { serve } from "https://deno.land/std@0.196.0/http/server.ts";
serve((req) => new Response("Hello Deno!"), { port: 8000 });

When a module is downloaded for the first time, it is cached locally, speeding up subsequent runs. Integrity checks ensure that cached modules haven't been tampered with, and versioning in URLs allows for reproducible builds and easy updates.

Deno fully embraces ES modules to align with modern JavaScript standards. This decision brings several benefits, such as:

  • Compatibility with browser JavaScript.
  • Better tree-shaking and static analysis capabilities.
  • A clearer code structure with explicit imports and exports.

As Deno projects scale, managing dependencies across multiple files can lead to version conflicts, inconsistencies, and slower performance due to repeated module fetches. To address this, Deno uses the deps.ts pattern to centralize dependencies in a single file:

 
// deps.ts
export { serve } from "https://deno.land/std@0.196.0/http/server.ts";

// main.ts
import { serve } from "./deps.ts";

Deno also provides vendoring capabilities to allow you to download and store all project dependencies locally. You can enable vendoring in your deno.json file with the following configuration:

deno.json
{
  "vendor": true
}

After vendoring, you can run your application offline by using the --cached-only flag:

 
deno run --cached-only app.ts

The Deno standard library

The Deno standard library is a curated collection of high-quality modules hosted on JSR and maintained by the Deno core team to ensure reliability and compatibility.

Each module in the library is independently versioned, allowing for isolated updates without affecting the entire library. The library also emphasizes cross-platform compatibility to ensure consistent functionality across different operating systems.

Screenshot of Deno runtime library

Some useful standard library modules include:

  • @std/fs: Utilities for interacting with the file system, making file operations straightforward.
  • @std/http: Tools for building and handling HTTP servers and requests, essential for web development.
  • @std/json: Efficient parsing and serialization of JSON data, including streaming operations.
  • @std/log: A versatile logging framework for customizable and structured logging.
  • @std/path: Utilities for working with file and directory paths, ensuring cross-platform compatibility.
  • @std/streams: Utilities for working with web streams.

For example, to use the logging module from the standard library:

app.js
import * as log from "@std/log";

log.debug("Debugging the application initialization.");
log.info("User ID 123456 logged in.");
log.critical("Critical failure: 500 Internal Server Error.");

Before using a standard library module, you must add the package:

 
deno add @std/log

Deno's compatibility with web platform APIs

Deno prioritizes implementing Web Platform APIs over proprietary features. This fosters a more unified and interoperable ecosystem, reducing fragmentation and promoting code reusability across JavaScript runtimes.

The following are some of the critical Web APIs supported by Deno. You can find the complete list in the official documentation:

Here's a simple example demonstrating how to use the Web Storage API in Deno:

app.js
// Store a value in localStorage
localStorage.setItem("username", "denoUser");

// Retrieve the value from localStorage
const username = localStorage.getItem("username");
console.log("Stored username:", username);

// Remove the value from localStorage
localStorage.removeItem("username");

// Verify removal
const removedUsername = localStorage.getItem("username");
console.log("Removed username:", removedUsername);

This demonstrates the basics of using localStorage in Deno. Unlike in browsers, Deno's local storage is an in-memory implementation that doesn't persist between program runs.

Deno's compatibility with Node.js

Deno initially didn't plan to support npm or Node.js modules, but in recent years, they've softened their stance (perhaps due to low adoption rates and strong competition from Bun) by introducing native support for running npm modules.

This decision significantly expands Deno's ecosystem and simplifies the transition for developers coming from Node.js. This means you can now easily migrate your Node.js to Deno enjoying Deno's security and modern tooling with minimal effort.

Here's some of the Deno features that allow you to run existing Node applications:

  • Native support for package.json, node_modules, and npm workspaces.
  • Native support for built-in Node.js modules like buffer, fs, worker_threads, process, and others.
 
import os from "node:os";
  • Package management with deno install, deno add, and deno remove commands.
  • Minor syntax adjustments needed to get your code working can be applied with deno lint --fix.
  • You can use npm packages without package.json and node_modules with the npm: specifier:
 
import express from "npm:express@4";

Or use deno.json:

 
// deno.json
{
  "imports": {
    "chalk": "npm:express@4"
  }
}
 
import express from "express";
  • Support for popular Node.js frameworks like Next.js, Astro, Remix, Angular, SvelteKit, QwikCity, and others.
  • Deno can execute CommonJS files while still enforcing its permission model:
 
deno run index.cjs

Deno vs Node.js performance

According to these benchmarks, Deno demonstrates a significant performance advantage over Node.js, as shown in the benchmark results:

Screenshot of the framework performance

Framework Mean RPS Max RPS Relative Performance
Deno (native) 68,851.87 90,944.36 87%
Node.js (native) 16,818.01 22,887.07 21%
Express (Deno) 11,116.47 13,183.14 14%
Express (Node) 6,329.47 7,907.90 8%

While these results highlight Deno's capabilities compared to Node.js, real-world performance can be influenced by various factors so ensure to run your own benchmarks.

Should you switch to Deno?

You've explored Deno, and now the question lingers: is it time to make the switch from Node.js?

The answer depends on your priorities. If you value modern standards, security, and a streamlined toolchain, with better performance to boot, Deno is a compelling choice, especially for new projects or when starting from scratch.

With the release of Deno 2, compatibility with Node.js has significantly improved, making migration or integration with existing Node.js projects easier than ever.

That said, Node.js remains a dominant force in JavaScript development, and its ecosystem is continuously evolving. Recent updates have introduced:

  • A built-in test runner.
  • An experimental permissions system, to address some security concerns.
  • Native TypeScript support (though not as seamless as Deno's).
  • Watch mode for better development workflow.
  • Native support for Web APIs like Fetch, WebSocket API, Web Streams, and more.

This means that many of the benefits that Deno offers are slowly becoming available in Node.js.

Ultimately, the choice between Deno and Node.js depends on your project's needs and your willingness to adopt newer technologies.

Whether you switch fully or experiment with Deno alongside Node.js, the growing competition between these platforms is great news for JavaScript developers everywhere.

To learn more about Deno, be sure to check out the documentation for further information.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Introduction to Bun for Node.js Users
This guide highlights Bun’s key features and shows how it can enhance your development workflow, making it a powerful alternative to Node.js
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github