Node and npm Compatibility (original) (raw)

Most Node.js code runs in Deno without modification. Here is a standard Node HTTP server, executed with deno instead of node:

main.mjs

import { createServer } from "node:http";

const server = createServer((req, res) => {
  res.end("Hello from Node-style code in Deno\n");
});

server.listen(3000, () => {
  console.log("Listening on http://localhost:3000/");
});

>_

$ deno run --allow-net main.mjs
Listening on http://localhost:3000/

Find the surface you care about in the overview below, then jump to its section for a working example, the rules behind it, and what to do when it doesn't work.

Compatibility at a glance Jump to heading

Surface Status Details
node: built-in modules Supported; nearly every module is implemented Use a Node built-in module
npm packages Supported, via npm: specifiers or package.json Using npm packages
Node globals (process, Buffer, __dirname) Supported; some need an explicit import Use Node globals
package.json (dependencies, scripts, type) Supported: dependencies, deno task scripts, type Run an existing Node project
CommonJS (require(), .cjs) Supported; module type detection differs in one case CommonJS support
node_modules layouts Optional; three modes, isolated or hoisted layout Control node_modules
Native addons (Node-API) Supported with local node_modules and --allow-ffi Use packages with native addons
.npmrc and registries Supported: private registries, auth tokens, mTLS Private registries, .npmrc configuration

As of Deno 2.8, over 75% of Node's own test suite passes in Deno, covering nearly every node: module. Most pure-JavaScript npm packages work without changes. The honest caveats: some APIs are partial, packages with native addons need a local node_modules directory, and a few tools assume npm's exact on-disk layout. The sections below cover each of those cases.

You can track the current state atnode-test-viewer.deno.dev and browse thelist of supported Node.js APIs.

Using npm packages Jump to heading

To use a package from npm, import it with an npm: prefix and run the file:

main.js

import * as emoji from "npm:node-emoji";

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

>_

$ deno run main.js
🦕 ❤️ npm

Deno downloads the package on first run and stores it in a global cache, so your project directory stays clean. The full specifier format is:

A few rules to know:

For examples with popular libraries, refer to thetutorial section.

Use a Node built-in module Jump to heading

Deno provides Node's built-in APIs through a compatibility layer. Import them with the node: prefix:

main.mjs

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.

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

Bare imports work too. Since Deno 2.9, a specifier that matches a Node built-in resolves to it even without the prefix, so import * as os from "os"runs with no prefix and no flag. Before 2.9 the bare form errored unless you passed --unstable-bare-node-builtins. Prefer the explicit node: form anyway: it is unambiguous, it is what the Deno LSP's quick-fixes insert, and it works in Node.js too. A deno.json imports entry or package.json dependency of the same name still wins over the built-in, and a node_modules package no longer shadows it, matching Node.js.

Explore built-in Node APIs

Use Node globals like process and Buffer Jump to heading

Node.js defines a number ofglobal objects available to all programs. Here is how the ones you will most often encounter in the wild map to Deno:

Node global In Deno What to do
process Available everywhere Use as-is, or import from node:process
Buffer Not global in your own code Import from node:buffer
__filename Not defined Use import.meta.filename
__dirname Not defined Use import.meta.dirname
require() Available in CommonJS files See CommonJS support
setTimeout / setInterval Available; Node semantics since Deno 2.8 Use as-is

Details on each:

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  
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 usingUint8Arrayor otherTypedArraysubclasses instead.

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

Run an existing Node project Jump to heading

Deno understands package.json, so a typical Node project runs with two commands: one to install, one to run.

package.json

{
  "type": "module",
  "scripts": {
    "start": "node main.js"
  },
  "dependencies": {
    "chalk": "^5"
  }
}

main.js

import chalk from "chalk";
console.log(chalk.green("ready"));

>_

$ deno install
$ deno run -R -E main.js
ready

deno install reads package.json and creates a node_modules directory, so bare imports like "chalk" resolve just as they do in Node. The -R and -Eflags grant the read and environment access that resolving node_modules (and chalk's color detection) need.

What carries over from your package.json:

Projects with a package.json default to the manual node_modules mode, which is why the explicit deno install step is needed. TheControl node_modules section covers the alternatives.

If it doesn't work: if you run before installing, Deno points you straight at the fix:

>_

$ deno run -R -E main.js
error: Could not resolve "chalk", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?
    at file:///my-app/main.js:1:19

Run deno install (or set "nodeModulesDir": "auto" to skip the explicit install step) and try again.

For a full checklist, optional toolchain improvements, and a Node-to-Deno command cheatsheet, see theMigrating from Node.js to Deno guide.

You can run npm CLI tools (packages with bin entries) directly, the way you would with npx:

>_

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

The specifier format accepts a subpath that selects a specific binary when a package ships several:

For example, deno run -R -E npm:cowsay@1.5.0/cowthink runs the cowthinkbinary from the same package. Scaffolding tools work the same way; this pair of commands is equivalent:

# 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 be scoped to npm via the `npm:` specifier.
$ deno run -A npm:create-next-app@latest

If it doesn't work: tools that read files or environment variables stop at a permission prompt (or fail in CI). Grant the specific permissions the tool needs, or -A for trusted scaffolding tools that need broad access.

CommonJS support Jump to heading

Deno supports CommonJS modules by default. Here is the smallest working setup for a .cjs file with an npm dependency:

deno.json

{
  "nodeModulesDir": "auto"
}

main.cjs

const express = require("express");
$ 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],
    ...
  }
}

Two requirements drive this recipe. CommonJS resolution expects dependencies to be installed in a local node_modules directory, which"nodeModulesDir": "auto" ensures (seeControl node_modules). And Deno's permission system applies to CommonJS code too: you typically need -R (--allow-read) and -E(--allow-env), because Deno probes package.json files and the node_modulesdirectory to resolve CommonJS modules.

How Deno decides a file is CommonJS Jump to heading

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

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 apackage.json file:

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" }.

Call require() from an ES module Jump to heading

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.

Mix CommonJS and ES modules 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

The other direction works too. You can 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" }

If it doesn't work: Deno guides you when a file looks like CommonJS but isn't loaded as such. Loading a .js file that uses require under a"type": "module" project gives you the fix in the error itself:

>_

$ deno run main.js
error: Uncaught (in promise) ReferenceError: require is not defined
const os = require("node:os");
           ^
    at file:///my-app/main.js:1:12

    info: Deno supports CommonJS modules in .cjs files, or when the closest
          package.json has a "type": "commonjs" option.
    hint: Rewrite this module to ESM,
          or change the file extension to .cjs,
          or add package.json next to the file with "type": "commonjs" option,
          or pass --unstable-detect-cjs flag to detect CommonJS when loading.
    docs: https://docs.deno.com/go/commonjs

Whenever you see an error about module or require not being defined, one of those four fixes applies:

Control 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 resolves npm packages from a central global 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).

Choose 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 ormanual mode if you get errors about missing packages inside the node_modulesdirectory.

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

Pick a 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.

Use packages with native addons Jump to heading

Deno supports Node-API addons used by popular npm packages like esbuild,npm:sqlite3 andnpm: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 localnode_modules/ directory is present. Configure"nodeModulesDir": "auto" | "manual" in deno.json or run with--node-modules-dir=auto|manual. And, like all native FFI, pass --allow-ffito grant explicit permission. ReviewSecurity and permissions.

If it doesn't work: many addons rely on npm lifecycle scripts (for example,postinstall) to build or download their native binding, and Deno does not run those scripts by default, for security reasons. Installing such a package warns you:

>_

$ deno install npm:duckdb
╭ Warning
│
│  Ignored build scripts for packages:
│  npm:duckdb@1.4.4
│
│  Run "deno approve-scripts" to run build scripts.
╰─

Ignore the warning and the addon fails at runtime, because the native binding the install script never fetched is missing:

>_

$ deno run -R -E --allow-ffi main.mjs
error: Uncaught (in promise) Error: Cannot find module '/my-app/node_modules/.deno/duckdb@1.4.4/node_modules/duckdb/lib/binding/duckdb.node'

The fix is to allow that specific package's scripts (or run the interactivedeno approve-scripts command):

>_

$ deno install --allow-scripts=npm:duckdb
Initialize duckdb@1.4.4: running 'install' script

$ deno run -R -E --allow-ffi main.mjs
[ { answer: 42 } ]

The deno install docs cover the lifecycle-scripts options in full.

Control package export conditions Jump to heading

Package exports can beconditioned 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, includingcreateRequire(), 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"
]

Get Node and npm type definitions 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" />

The same directive lets you use types from the NodeJS namespace, likeBufferEncoding, in your own signatures:

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

Types for npm packages 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@typespackage:

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

When a package's types look wrong Jump to heading

The official TypeScript compiler tsc supports differentmoduleResolutionsettings. 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 withtsc --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 alsomicrosoft/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-erroror // @ts-ignore.

Private registries Jump to heading

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:

.npmrc configuration Jump to heading

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

//registry.mycompany.com/:certfile=/etc/deno/client.crt  
//registry.mycompany.com/:keyfile=/etc/deno/client.key  
//registry.mycompany.com/:_auth=secretToken  
//registry.mycompany.com/:email=ci@mycompany.com  

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.