Skip to main content
On this page

Node and npm Compatibility

Deno is Node-compatible: most Node.js projects will run in Deno with little or no change. You can import npm packages with the npm: specifier, use Node's built-in modules through node: specifiers, and keep your existing package.json. No npm install step is required before running your code.

main.ts
import chalk from "npm:chalk@5";
console.log(chalk.green("Hello from npm in Deno"));
>_
deno run main.ts

As of Deno 2.8, over 75% of Node's own test suite passes in Deno, covering nearly every node: module. You can track the current state at node-test-viewer.deno.dev and browse the list of supported Node.js APIs.

That's all you really need to know to get started. The rest of this page covers the details, along with some key differences between the two runtimes that you can take advantage of to make your code simpler and smaller when migrating your Node.js projects to Deno.

Using Node's built-in modules Jump to heading

Deno provides a compatibility layer that allows the use of Node.js built-in APIs within Deno programs. However, in order to use them, you will need to add the node: specifier to any import statements that use them:

import * as os from "node:os";
console.log(os.cpus());

Run it with deno run main.mjs and you will get the same output as running the program in Node.js.

Updating any imports in your application to use node: specifiers should enable any code using Node built-ins to function as it did in Node.js.

To make updating existing code easier, Deno will provide helpful hints for imports that don't use the node: prefix:

main.mjs
import * as os from "os";
console.log(os.cpus());
>_
$ deno run main.mjs
error: Import "os" not a dependency
  hint: If you want to use a built-in Node module, add a "node:" prefix (ex. "node:os").
    at file:///main.mjs:1:21

The same hints and additional quick-fixes are provided by the Deno LSP in your editor.

The node:module built-in includes the registerHooks() API, which you can use to customize module resolution and loading from inside your program.

Explore built-in Node APIs

Using npm packages Jump to heading

Deno has native support for importing npm packages by using npm: specifiers, which have the following format:

npm:[@][/]

For example:

main.js
import * as emoji from "npm:node-emoji";

console.log(emoji.emojify(`:sauropod: :heart:  npm`));

Can be run with:

>_
$ deno run main.js
🦕 ❤️ npm

No npm install is necessary before the deno run command and no node_modules folder is created: dependencies are stored in Deno's global cache. These packages are also subject to the same permissions as other code in Deno. If your tools expect a local node_modules directory, see node_modules.

npm specifiers also allow functionality that may be familiar from the npx command.

# npx allows remote execution of a package from npm or a URL
$ npx create-next-app@latest

# deno run allows remote execution of a package from various locations,
# and can scoped to npm via the `npm:` specifier.
$ deno run -A npm:create-next-app@latest

For examples with popular libraries, please refer to the tutorial section.

First-class package.json support Jump to heading

Deno understands package.json in your project. You can:

  • Declare dependencies there (alongside or instead of inline npm: specifiers).
  • Use scripts from package.json via deno task (for example, deno task start).
  • Rely on package.json fields like type when resolving modules (see CommonJS support).

Node.js global objects Jump to heading

In Node.js, there are a number of global objects available in the scope of all programs that are specific to Node.js, eg. process object.

Here are a few globals that you might encounter in the wild and how to use them in Deno:

  • process - Deno provides the process global, which is by far the most popular global used in popular npm packages. It is available to all code. Deno can also guide you towards importing it explicitly from node:process. Opt in by enabling the no-process-global lint rule (off by default since Deno 2.8); deno lint will then flag uses of the global:
process.js
console.log(process.versions.deno);
$ deno run process.js
2.8.3
$ deno lint process.js
error[no-process-global]: NodeJS process global is discouraged in Deno
 --> /process.js:1:13
  |
1 | console.log(process.versions.deno);
  |             ^^^^^^^
  = hint: Add `import process from "node:process";`

  docs: https://docs.deno.com/lint/rules/no-process-global


Found 1 problem (1 fixable via --fix)
Checked 1 file
  • require() - see CommonJS support

  • Buffer - to use Buffer API it needs to be explicitly imported from the node:buffer module:

buffer.js
import { Buffer } from "node:buffer";

const buf = new Buffer(5, "0");

For TypeScript users needing Node.js-specific types like BufferEncoding, these are available through the NodeJS namespace when using @types/node:

buffer-types.ts
/// <reference types="npm:@types/node" />

// Now you can use NodeJS namespace types
function writeToBuffer(data: string, encoding: NodeJS.BufferEncoding): Buffer {
  return Buffer.from(data, encoding);
}

Prefer using Uint8Array or other TypedArray subclasses instead.

  • __filename - use import.meta.filename instead.

  • __dirname - use import.meta.dirname instead.

  • setTimeout / setInterval - starting in Deno 2.8, the global timer functions return a Node.js Timeout object instead of a number, matching Node.js semantics. The returned object exposes methods like .ref(), .unref(), .refresh(), and .hasRef(). It still coerces to a number (via Symbol.toPrimitive), so existing code that stores the timer ID as a number or passes it to clearTimeout/clearInterval continues to work unchanged.

    const t = setTimeout(() => {}, 1000);
    t.unref(); // don't keep the event loop alive for this timer
    clearTimeout(t);
    

CommonJS support Jump to heading

Deno supports CommonJS modules by default. When using CommonJS, Deno expects that dependencies will be installed manually and a node_modules directory will be present. It's best to set "nodeModulesDir": "auto" in your deno.json to ensure that (see node_modules).

Deno's permission system also applies to CommonJS code. You typically need the -R (--allow-read) and -E (--allow-env) flags, because Deno probes package.json files and the node_modules directory to resolve CommonJS modules.

How Deno determines module type Jump to heading

If the file extension is .cjs, Deno will treat the module as CommonJS, without consulting package.json:

main.cjs
const express = require("express");

Deno will also attempt to load .js, .jsx, .ts, and .tsx files as CommonJS if there's a package.json file with the "type": "commonjs" option next to the file, or up in the directory tree when in a project with a package.json file:

package.json
{
  "type": "commonjs"
}
main.js
const express = require("express");

Tools like Next.js's bundler and others will generate a package.json file like that automatically. If you have an existing project that uses CommonJS modules, you can make it work with both Node.js and Deno by adding the "type": "commonjs" option to the package.json file.

Deno does not otherwise analyze module contents to detect CommonJS, because looking for package.json files on the file system and analyzing a module to detect if it's CommonJS takes longer than not doing it, and to discourage the use of CommonJS. You can opt into this detection by running with the --unstable-detect-cjs flag in Deno >= 2.1.2. It takes effect except when there's a package.json file with { "type": "module" }.

Using require() Jump to heading

To run the .cjs example above, install the dependency and pass the read and env permission flags:

$ cat deno.json
{
  "nodeModulesDir": "auto"
}

$ deno install npm:express
Dependencies:
+ npm:express 5.2.1

$ deno run -R -E main.cjs
[Function: createApplication] {
  application: {
    init: [Function: init],
    defaultConfiguration: [Function: defaultConfiguration],
    ...
  }
}

In ES modules, you can create an instance of the require() function manually:

main.js
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const express = require("express");

In this scenario the same requirements apply as when running .cjs files: dependencies need to be installed manually and appropriate permission flags given.

CommonJS and ESM interop Jump to heading

Deno's require() implementation supports requiring ES modules. This works the same as in Node.js, where you can only require() ES modules that don't have top-level await in their module graph. In other words, you can only require() ES modules that are "synchronous".

greet.js
export function greet(name) {
  return `Hello ${name}`;
}
esm.js
import { greet } from "./greet.js";

export { greet };
main.cjs
const esm = require("./esm");
console.log(esm);
console.log(esm.greet("Deno"));
$ deno run -R main.cjs
[Module: null prototype] { greet: [Function: greet] }
Hello Deno

You can also import CommonJS files in ES modules:

greet.cjs
module.exports = {
  hello: "world",
};
main.js
import greet from "./greet.cjs";
console.log(greet);
$ deno run main.js
{ hello: "world" }

Troubleshooting Jump to heading

Deno will guide you when a file looks like CommonJS but isn't loaded as such. If you see an error about module not being defined, fix it by one of the following:

  • Rewrite the code to ESM
  • Change the file extension to .cjs
  • Add a nearby package.json with { "type": "commonjs" }
  • Run with --unstable-detect-cjs

Conditional exports Jump to heading

Package exports can be conditioned on the resolution mode. The conditions satisfied by an import from a Deno ESM module are as follows:

["deno", "node", "import", "module-sync", "default"]

This means that the first condition listed in a package export whose key equals any of these strings will be matched. For require() resolution, including createRequire(), the conditions are:

["require", "node", "module-sync", "default"]

Deno also applies module-sync when analyzing CommonJS modules that re-export through require().

You can expand the import conditions list using the --conditions CLI flag:

deno run --conditions development,react-server main.ts
[
  "development",
  "react-server",
  "deno",
  "node",
  "import",
  "module-sync",
  "default"
]

Importing types Jump to heading

Many npm packages ship with types, you can import these and use them with types directly:

import chalk from "npm:chalk@5";

Some packages do not ship with types but you can specify their types with the @ts-types directive. For example, using a @types package:

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

Module resolution Jump to heading

The official TypeScript compiler tsc supports different moduleResolution settings. Deno only supports the modern node16 resolution. Unfortunately many npm packages fail to correctly provide types under node16 module resolution, which can result in deno check reporting type errors, that tsc does not report.

If a default export from an npm: import appears to have a wrong type (with the right type seemingly being available under the .default property), it's most likely that the package provides wrong types under node16 module resolution for imports from ESM. You can verify this by checking if the error also occurs with tsc --module node16 and "type": "module" in package.json or by consulting the Are the types wrong? website (particularly the "node16 from ESM" row).

If you want to use a package that doesn't support TypeScript's node16 module resolution, you can:

  1. Open an issue at the issue tracker of the package about the problem. (And perhaps contribute a fix :) (Although, unfortunately, there is a lack of tooling for packages to support both ESM and CJS, since default exports require different syntaxes. See also microsoft/TypeScript#54593)
  2. Use a CDN, that rebuilds the packages for Deno support, instead of an npm: identifier.
  3. Ignore the type errors you get in your code base with // @ts-expect-error or // @ts-ignore.

Including Node types Jump to heading

Starting in Deno 2.8, deno check and the LSP include lib.node in every type-check by default, so Node ambient types like Buffer, NodeJS.Timeout, and process resolve without any configuration:

// 2.8+: type-checks with no extra setup
const buf: Buffer = Buffer.from("hello");
const t: NodeJS.Timeout = setTimeout(() => {}, 0);

The bundled lib.node tracks the major version of @types/node that matches the Node release Deno reports in process.versions.node. If you need to pin a specific @types/node version (for example to match the Node version your project standardizes on), add it as an explicit dependency:

deno.json
{
  "imports": {
    "@types/node": "npm:@types/node@^22"
  }
}

On versions before 2.8, or if you've opted out of lib.node, you can still load the types with a reference directive:

/// <reference types="npm:@types/node" />

Run npm binaries Jump to heading

You can run npm CLI tools (packages with bin entries) directly without npm install by using an npm: specifier:

npm:[@][/]

For example:

>_
$ deno run -R npm:cowsay@1.5.0 "Hello there!"
 ______________
< Hello there! >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

$ deno run -R npm:cowsay@1.5.0/cowthink "What to eat?"
 ______________
( What to eat? )
 --------------
        o   ^__^
         o  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

node_modules Jump to heading

When you run npm install, npm creates a node_modules directory in your project which houses the dependencies as specified in the package.json file.

By default, Deno instead uses npm specifiers to resolve npm packages to a central global npm cache and does not create a node_modules directory. This uses less space, keeps your project directory clean, and is the recommended setup for new Deno projects.

There may however be cases where you need a local node_modules directory in your Deno project, even if you don't have a package.json (eg. when using frameworks like Next.js or Svelte or when depending on npm packages that use Node-API).

Choosing a node_modules mode Jump to heading

Mode When to use How to enable
none Most Deno projects; keep repo clean Default; do nothing
auto Tools/bundlers expect node_modules; Node-API "nodeModulesDir": "auto" or --node-modules-dir=auto
manual Existing package.json with install step "nodeModulesDir": "manual" + run deno install/npm/pnpm

Note

We recommend that you use the default none mode, and fall back to auto or manual mode if you get errors about missing packages inside the node_modules directory.

Automatic node_modules creation Jump to heading

If you need a node_modules directory in your project, you can use the --node-modules-dir=auto flag on a per-command basis, or the "nodeModulesDir": "auto" option in the config file, to tell Deno to create a node_modules directory in the current working directory:

>_
deno run --node-modules-dir=auto main.ts

or with a configuration file:

deno.json
{
  "nodeModulesDir": "auto"
}

The auto mode automatically installs dependencies into the global cache and creates a local node_modules directory in the project root. This is recommended for projects that have npm dependencies that rely on the node_modules directory: mostly projects using bundlers, or ones that have npm dependencies with postinstall scripts.

Manual node_modules creation Jump to heading

If your project has a package.json file, you can use the manual mode, which requires an installation step to create your node_modules directory:

>_
deno install
deno run --node-modules-dir=manual main.ts

or with a configuration file:

deno.json
{ "nodeModulesDir": "manual" }

You would then run deno install/npm install/pnpm install or any other package manager to create the node_modules directory.

Manual mode is the default mode for projects using a package.json. You may recognize this workflow from Node.js projects. It is recommended for projects using frameworks like Next.js, Remix, Svelte, Qwik etc, or tools like Vite, Parcel or Rollup.

node_modules layout: isolated vs hoisted Jump to heading

When a local node_modules directory exists, Deno can lay it out in two ways. The default (isolated) installs each package into a content-addressed .deno/ directory and exposes it through a symlink, so every package only sees its declared dependencies. This is similar to pnpm's layout.

node_modules/
├── .deno/chalk@5.6.2/node_modules/chalk/   ← real files
└── chalk -> .deno/chalk@5.6.2/node_modules/chalk

Some npm tooling, and any package that walks node_modules looking for flat-resolved siblings, assumes the hoisted layout that npm and Yarn classic use. Deno 2.8 adds a hoisted mode (denoland/deno#32788) you can opt into with nodeModulesLinker in deno.json. The hoisted linker requires a manually-managed node_modules directory, so set nodeModulesDir to manual:

deno.json
{
  "nodeModulesDir": "manual",
  "nodeModulesLinker": "hoisted"
}

Or as a one-off CLI flag (also requiring --node-modules-dir=manual):

>_
deno install --node-modules-dir=manual --node-modules-linker=hoisted

In hoisted mode the most-depended-upon version of each package is placed at the top of node_modules/, and conflicting versions are nested under the dependent that needs them, just like npm:

node_modules/
├── chalk/         ← real files
├── express/
├── ms/            ← hoisted: most commonly needed version
└── debug/
    └── node_modules/
        └── ms/    ← nested: a different version

Stick with the default isolated mode unless a tool you depend on requires the hoisted layout. Isolated mode catches phantom dependencies that hoisted layouts hide.

Node-API addons Jump to heading

Deno supports Node-API addons used by popular npm packages like esbuild, npm:sqlite3 and npm:duckdb. You can expect packages that use public Node-APIs to work.

As of Deno 2.0, npm packages using Node-API addons are supported when a local node_modules/ directory is present. Configure "nodeModulesDir": "auto" | "manual" in deno.json or run with --node-modules-dir=auto|manual.

Like all native FFI, pass --allow-ffi to grant explicit permission. Review Security and permissions.

Note

Many addons rely on npm lifecycle scripts (for example, postinstall). Deno supports them, but they are not run by default for security reasons. See the deno install docs.

Migrating from Node to Deno Jump to heading

Running a Node.js project with Deno usually requires little to no change. See the Migrating from Node.js to Deno guide for the details, optional toolchain improvements, and a Node-to-Deno command cheatsheet.

Private registries Jump to heading

Caution

Not to be confused with private repositories and modules.

Deno supports private registries, which allow you to host and share your own modules. This is useful for organizations that want to keep their code private or for individuals who want to share their code with a select group of people.

What are private registries? Jump to heading

Large organizations often host their own private npm registries to manage internal packages securely. These private registries serve as repositories where organizations can publish and store their proprietary or custom packages. Unlike public npm registries, private registries are accessible only to authorized users within the organization.

How to use private registries with Deno Jump to heading

First, configure your .npmrc file to point to your private registry. The .npmrc file must be in the project root or $HOME directory. Add the following to your .npmrc file:

>_
@mycompany:registry=http://mycompany.com:8111/
//mycompany.com:8111/:_authToken=secretToken

Replace http://mycompany.com:8111/ with the actual URL of your private registry and secretToken with your authentication token. _authToken is the standard bearer-token form; registries that use legacy _auth credentials are also supported (see the .npmrc features list below).

Then update Your deno.json or package.json to specify the import path for your private package. For example:

deno.json
{
  "imports": {
    "@mycompany/package": "npm:@mycompany/package@1.0.0"
  }
}

or if you're using a package.json:

package.json
{
  "dependencies": {
    "@mycompany/package": "1.0.0"
  }
}

Now you can import your private package in your Deno code:

main.ts
import { hello } from "@mycompany/package";

console.log(hello());

and run it using the deno run command:

>_
deno run main.ts

.npmrc configuration Jump to heading

Beyond the basic registry / token setup above, Deno reads several other .npmrc fields. The ones most likely to matter:

  • Mutual-TLS authentication (Deno 2.8+): certfile and keyfile point at PEM files used to authenticate the client when the registry requires mTLS.

    .npmrc
    //registry.mycompany.com/:certfile=/etc/deno/client.crt
    //registry.mycompany.com/:keyfile=/etc/deno/client.key
    
  • email on _auth entries (Deno 2.8+): some legacy on-prem registries require an email alongside the auth token.

    .npmrc
    //registry.mycompany.com/:_auth=secretToken
    //registry.mycompany.com/:email=ci@mycompany.com
    
  • min-release-age (Deno 2.8+): refuses to install package versions younger than the configured age. Useful as a default supply-chain guard for all installs. The same control is also available as the CLI flag --minimum-dependency-age and the minimumDependencyAge field in deno.json. See Minimum dependency age for the full picture.

    .npmrc
    min-release-age=3
    
  • NPM_CONFIG_REGISTRY env var: overrides the registry set in .npmrc, matching npm's precedence (handy in CI when you want to redirect installs without editing the checked-in .npmrc).

file: and link: dependencies in published packages Jump to heading

Some published npm packages accidentally ship a file: or link: specifier in their package.json that points at a path on the publisher's machine:

some-package/package.json
{
  "dependencies": {
    "lodash": "^4.17.0",
    "local-helpers": "file:../local-helpers"
  }
}

Starting in Deno 2.8, those file: and link: entries are silently skipped while resolving npm metadata, so packages that carry a stray local-path dependency install cleanly instead of failing with an "Invalid version requirement" error.

Last updated on

Did you find what you needed?

Edit this page
Privacy policy