Back to Scaling Node.js Applications guides

Deno Overview for Node.js Users

Stanley Ulili
Updated on September 10, 2024

Deno is an open-source runtime for JavaScript and TypeScript, created to address some of the limitations of Node.js and boost developer productivity. It offers native TypeScript support, a secure permissions-based sandbox, and built-in tools like code formatter, linter and test runner. Deno also simplifies dependency management by removing the need for npm while still being compatible with the Node.js ecosystem.

This guide will introduce you to Deno’s features and help you understand how it compares to Node.js.

Prerequisites

To follow this tutorial, ensure you have:

  • Deno installed on your system. You can refer to the installation instructions.
  • A basic understanding of JavaScript and experience building applications with it.

Understanding Deno

Deno was introduced as an alternative to Node.js, a popular runtime for building server-side applications. Both were created by Ryan Dahl, who developed Deno to address various shortcomings he identified in Node.js. In a 2018 talk, Dahl highlighted issues such as the lack of a unified approach to asynchronous programming with Promises, missed opportunities for stronger security, the complexities of the build system (GYP), and challenges related to the module system, including package.json, node_modules, and implicit file extensions.

Deno was designed to address these issues by providing a more secure, modern, and streamlined runtime. It enhances security by requiring explicit permissions for file system and network access. The module system is simplified, using only relative or absolute URLs with required extensions. Deno integrates TypeScript support natively and is distributed as a single executable with minimal dependencies. It adheres to web standards, supports the creation of standalone executables, and is built on V8, Rust, and Tokio. Additionally, Deno maintains backward compatibility with Node.js and supports millions of npm modules.

Getting started with Deno

In this section, you'll initialize a directory with Deno, write a simple program, and execute it.

First, verify your installation with:

 
deno --version

This should produce an output similar to:

Output
deno 1.46.1 (stable, release, x86_64-unknown-linux-gnu)
v8 12.9.202.2-rusty
typescript 5.5.2

This confirms that Deno, its embedded V8 engine, and TypeScript are installed correctly.

If you already have Deno installed, ensure you’re using the latest version by running:

 
deno upgrade

With Deno installed, you can initialize a project in any directory you choose. Unlike Node.js, where you would use npm init -y to set up a project, with Deno, you simply run the following command:

 
deno init

This command creates a few default files: deno.json for configuration, main.ts, and main_test.ts. These are basic templates to help you get started.

For now, we'll skip TypeScript and create a simple app.js file with the following content:

app.js
function welcomeUser(user) {
  return `Hello, ${user.name}!`;
}
console.log(welcomeUser({ name: "Alice" }));

You can run this program using the deno run command:

 
deno run app.js

This will output:

Output
Hello, Alice!

Now that you know how to initialize a directory and run a basic program, you're ready to explore the many features and benefits Deno offers, starting with its tooling.

Tooling

Deno offers a comprehensive suite of built-in tools that are typically separate installations in other runtimes like Node.js. This integration simplifies development and reduces the overhead of managing dependencies, making Deno a powerful choice for developers seeking an all-in-one solution.

REPL (Read-Eval-Print Loop)

Deno includes a robust REPL for quick experimentation and debugging. Unlike many other environments, Deno’s REPL supports TypeScript natively, allowing you to write and test TypeScript code directly without prior compilation:

 
deno
Output
Deno 1.46.1
exit using ctrl+d, ctrl+c, or close()
REPL is running with all permissions allowed.
To specify permissions, run `deno repl` with allow flags.

This makes Deno particularly handy for testing out TypeScript features or snippets on the fly.

File watcher

Deno's file watcher automatically restarts your application when it detects changes in your code. This feature is especially useful in server-side development, where rapid iteration and immediate feedback are crucial:

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

In addition, Deno supports Hot Module Replacement (HMR), allowing you to update your application without a complete restart. You can enable this feature using the --watch-hmr flag:

 
deno run --allow-net --watch-hmr app.js

Linter

One of the features that sets Deno apart from Node.js is its built-in linter, eliminating the need for external tools like ESLint.

You can use it with the following command:

 
deno lint

You can also lint specific files:

 
deno lint myfile1.ts myfile2.ts myfile3.ts

The built-in linter’s seamless integration with Deno means fewer dependencies and a simpler setup for maintaining code hygiene.

Formatter

Another tool unique to the Deno runtime is its built-in formatter, which ensures your code adheres to a consistent style across your project. This is particularly beneficial in team environments where maintaining a uniform codebase is crucial:

 
deno fmt

You can format specific files as well:

 
deno fmt myfile1.ts myfile2.ts myfile3.ts

The formatter enforces opinionated rules that promote clean and readable code, reducing the potential for style-related disputes in collaborative projects.

Test Runner

Deno’s test runner is a powerful tool that supports both JavaScript and TypeScript. It includes features like mocking, test coverage, and built-in assertions, making it a comprehensive solution for testing your applications. The test runner automatically detects files that match common naming conventions:

  • test.{js, ts, jsx, mjs, mts, tsx}
  • *_test.{js, ts, jsx, mjs, mts, tsx}
  • *.test.{js, ts, jsx, mjs, mts, tsx}

Here’s an example test file app_test.js:

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);
});

Run tests using:

 
deno test

Output:

Output
Check file:///home/stanley/deno-demo/app_test.js
Check file:///home/stanley/deno-demo/main_test.ts
running 1 test from ./app_test.js
Add two numbers ... ok (1ms)
running 1 test from ./main_test.ts
addTest ... ok (0ms)

ok | 2 passed | 0 failed (81ms)

As you can see, it ran both the test you added and the test included during initialization.

Compile command for creating executables

Deno allows you to compile your TypeScript or JavaScript code into standalone executables, compared to the more complex steps required in Node.js’s single executable applications. With Deno, you can easily create an executable in one line, enabling users to run the app without needing Deno installed:

 
deno compile --allow-net --output server app.js

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

 
./server

This capability simplifies deployment and distribution, particularly for end-users who may not be familiar with Deno or its ecosystem.

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

One of Deno's most notable features is its secure-by-default approach. Unlike other runtimes, such as 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.

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.ts

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.ts

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.

Using these configuration options helps create a consistent and efficient development environment and reinforces best practices and coding standards across your project. This level of flexibility and control is one of Deno's key strengths, making it a powerful tool for modern web development.

Integrating these configurations into your workflow allows you to optimize your project setup, reduce manual configuration efforts, and ensure that your development environment is precisely tailored to your needs.

Built-in support for TypeScript

Deno offers first-class support for TypeScript with zero configuration, allowing you to write type-safe programs without complex build systems. This is made possible by Deno’s internal system, which optimally compiles TypeScript directly into JavaScript without requiring additional tools like Babel. The compiled JavaScript is then cached, so 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.

Deno handles TypeScript configuration differently from tsc, only recognizing specific compilerOptions while ignoring others that might be irrelevant or problematic in the Deno environment. You can view the full list of supported options.

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

deno.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "lib": ["deno.window"]
  }
}

To apply this configuration when running your TypeScript file, use:

 
deno run --config deno.json app.ts

This configuration file allows you to tailor TypeScript’s behaviour to your needs while ensuring compatibility with Deno’s built-in systems.

Now that you're familiar with Deno's capabilities with TypeScript, you can explore how dependency management works in Deno.

How dependency management works in Deno

Deno introduces a unique approach to dependency management, distinct from Node.js, 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 method 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; and decentralization, 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 });

Deno fully embraces ES modules, aligning with modern web standards. This decision brings several benefits, such as:

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

Additionally, the requirement to include file extensions enhances clarity and performance:

 
import { parse } from "./utils/parser.ts";

Deno’s caching mechanism offers significant performance improvements. 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.

For instance:

 
import { Application } from "https://deno.land/x/oak@v12.6.0/mod.ts";

As Deno projects grow in complexity, managing dependencies can become challenging. Multiple files importing various modules from different sources can lead to version conflicts, inconsistencies, and difficulty in updating dependencies. Additionally, repeatedly fetching the same modules across different files can impact load times and performance.

The Deno community has adopted the deps.ts pattern to address these issues. This approach centralizes dependency management in a single file, offering several benefits:

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

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

The deps.ts file is a single source of truth for external dependencies, simplifying version management across the entire project. Consolidating imports reduces the number of remote fetches, potentially improving load times. Moreover, it makes updating dependencies more straightforward, as changes only need to be made in one place.

This file is a lightweight alternative to the package.json found in Node.js projects, tailored to Deno's URL-based import system. This approach streamlines dependency management while maintaining Deno's philosophy of simplicity and explicit imports.

When developing software, relying on external dependencies hosted on remote servers can introduce risks such as downtime, security vulnerabilities, and inconsistent builds due to network issues. Deno addresses these challenges through its vendoring feature, allowing developers 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
}

This will create a ./vendor folder where all dependencies are stored. This setup ensures offline availability, provides consistent builds across different environments, enhances security through easier auditing of dependencies, and speeds up build times by removing the need to fetch dependencies from remote sources. You can also run the following command to cache dependencies immediately:

 
deno cache app.ts

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

 
deno run --cached-only app.ts

This ensures that Deno uses only the locally available modules, eliminating the need for internet access during runtime.

Alternatively, you can use the deno vendor subcommand to achieve similar results, though this method is more cumbersome and is planned for deprecation, as noted in the Deno issue tracker.

The standard library

The Deno standard library is a curated collection of high-quality modules hosted on JSR (https://jsr.io/@std), maintained by the Deno core team to ensure reliability and compatibility. It reduces the need for external dependencies, allowing developers to build robust applications with fewer third-party modules, simplifying maintenance and lowering security risks.

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

Some functional 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 streams, facilitating memory-efficient data processing.

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

This command adds an import map entry, making your import code cleaner.

When you run the program:

 
deno run app.js

You’ll see the following log messages:

Output
INFO User ID 123456 logged in.
CRITICAL Critical failure: 500 Internal Server Error.

This example demonstrates how easily the standard library's logging module can be integrated into your projects, providing structured and customizable logging out of the box.

Web platform APIs

Deno prioritizes implementing Web Platform APIs over proprietary features, offering several benefits. This approach makes Deno more familiar to developers with browser experience, allowing them to apply their existing knowledge seamlessly.

By adhering to web standards, Deno promotes compatibility and reduces fragmentation, enabling many existing JavaScript libraries designed for browsers to work in Deno with minimal modifications. This strategy also future-proofs the platform, making it easier for Deno to adopt web standards changes and keep the runtime modern and relevant.

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

These APIs fully integrate into Deno, allowing developers to build web applications and services using familiar, standardized tools.

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);

When you run the program, you’ll see the output showing that you can store and retrieve data in local storage:

 
deno run app.js
Output
Stored username: denoUser
Removed username: null

This example demonstrates the basics of using localStorage in Deno. Unlike in browsers, Deno’s local storage is typically an in-memory implementation that doesn’t persist between program runs. You might want to explore Deno’s file system APIs or use a database for persistent storage.

Compatibility with Node.js

Deno initially didn't plan to support npm or Node.js modules, but last year, they announced native support for running npm modules. This decision greatly expands Deno's ecosystem and simplifies the transition for developers moving from Node.js. Here’s how you can take advantage of this compatibility:

  • npm: specifiers and node: specifiers
  • Deno compatibility with package.json
  • Using CDNs

Deno allows you to import npm packages using the npm: specifier, removing the need for npm install and a node_modules directory.

For example, you can easily use Express from npm in your Deno project:

 
import express from "npm:express@4";
const app = express();

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(3000);

All npm packages in Deno run under the same strict permission model as Deno’s native code, maintaining the platform’s security-first approach. If a package lacks TypeScript definitions, you can specify them with the @deno-types directive:

 
// @deno-types="npm:@types/express@^4.17"
import express from "npm:express@^4.17";

Deno also natively supports many of Node.js’s built-in modules, such as buffer, fs, sys, and worker threads. These can be imported using the node: specifier, enabling you to use familiar Node.js core modules in your Deno projects:

 
import os from "node:os";

Deno supports the use of package.json, making it easier to run Node.js projects within Deno while retaining Deno’s unique advantages. This compatibility allows Deno to understand and use npm dependencies specified in package.json, simplifying porting existing Node.js projects to Deno.

In addition to npm and Node.js built-ins, Deno supports importing modules directly from content delivery networks (CDNs) such as ESM, Skypack, and JSPM. This method leverages Deno's modern module system, allowing for flexible integration of JavaScript libraries:

 
import { fs } from "https://esm.sh/file-system";

This approach combines the extensive npm ecosystem with Deno’s ability to import modules directly from URLs, providing developers with multiple options to integrate existing libraries into their Deno projects.

Performance

This section analyzes the benchmarks, comparing the performance of Deno and Node.js, including their respective implementations of Express.js. The results reveal significant differences in their capabilities, as shown in the table below:

Framework Mean RPS Max RPS Relative Performance
Deno 68,851.87 90,944.36 87%
Node.js 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%

Deno stands out in terms of raw performance, averaging 68,851.87 requests per second, significantly outpacing Node.js, which averages 16,818.01 requests per second. This large performance gap highlights the efficiency of Deno's modern architecture and optimizations.

Even within the Express.js framework, Deno continues to lead, handling 11,116.47 requests per second, compared to Express on Node.js, which manages 6,329.47 requests per second. This demonstrates that Deno's performance benefits extend beyond just its core runtime and into widely used web frameworks.

Deno's superior performance can be attributed to several key factors. Built on Rust and the V8 JavaScript engine, Deno leverages cutting-edge technologies that boost execution speed. Its modern JavaScript support and security-first design further enhance performance.

However, while these benchmarks underscore Deno's strengths, real-world performance may vary depending on specific use cases and deployment scenarios.

Should you switch to Deno?

Deciding whether to switch to Deno for your next project requires carefully evaluating its benefits and potential challenges. Deno offers several advantages, including modern features like built-in TypeScript support, enhanced security with default sandboxing, and in some cases, improved performance.

These make Deno particularly appealing for projects emphasising security and using the latest JavaScript features. The recent addition of support for npm packages and package.json has also made transitioning from Node.js easier, broadening Deno's appeal to developers familiar with the Node.js ecosystem.

However, Deno's ecosystem is still less mature compared to Node.js, with fewer available packages and resources. This can lead to compatibility issues, especially for larger or more complex applications. Although Deno supports npm to an extent, not all Node.js packages work seamlessly, which might limit Deno's usefulness in certain scenarios.

Meanwhile, Node.js continues to evolve, adding features like an experimental permissions system, native environment variable support, a watch mode, and a built-in test runner. These improvements ensure that Node.js remains a strong, reliable choice for various applications.

Deno could be an excellent choice for smaller projects focused on security and modern JavaScript practices, where its forward-thinking design truly shines. However, for projects that are likely to grow or require a stable, well-supported runtime, Node.js still offers significant advantages. The choice ultimately depends on your project's specific needs, your team's expertise, and your long-term development goals. While Deno shows great promise and continues to improve, Node.js's maturity and extensive ecosystem of Node.js provide compelling reasons to stick with it.

Final thoughts

This article has explored Deno's unique features and how it addresses many of the issues found in Node.js. If you're considering using Deno, you should now have a solid understanding of how to get started and how it compares to Node.js, helping you choose the best tool for your project.

If you're interested in learning more about Deno, be sure to check out the documentation for further information.

Author's avatar
Article by
Stanley Ulili
Stanley is a freelance web developer and researcher from Malawi. He loves learning new things and writing about them to understand and solidify concepts. He hopes that by sharing his experience, others can learn something from them too!
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