Skip to main content

Deno 3 Features and npm Compatibility Guide 2026

·PkgPulse Team
0

Deno 3 shipped with a clear thesis: full npm compatibility without compromising Deno's security model. The previous versions made incremental progress — Deno 1.28 added npm: specifiers, Deno 2 added node_modules directory support — but gaps remained. Deno 3 closes most of them. Here's what's new and how to migrate existing Node.js projects.

TL;DR

Deno 3 adds workspaces (monorepo support), full node_modules compatibility (including native addons via .node files), 30-40% faster cold starts from the V8 snapshot overhaul, and a deno install command that generates a lockfile and node_modules directory for npm packages. The practical result: most Node.js projects run on Deno 3 with zero or minimal code changes.

Key Changes

FeatureDeno 2Deno 3
npm compatibility~85% of top 1000 packages~97% of top 1000 packages
WorkspacesNot supporteddeno.json workspaces
Native addons (.node)Not supportedSupported via FFI bridge
node_modules modeOpt-in flagDefault for npm-heavy projects
Cold start time~45ms~28ms
package.json supportPartial (scripts only)Full (including exports map)
deno installNot availableFull dependency installer

Workspaces: Monorepo Support

Deno 3's workspace support lets you manage multiple packages in a single repository with shared dependencies and unified configuration. The setup is a deno.json at the repo root with a workspaces array:

{
  "workspaces": ["./packages/core", "./packages/api", "./packages/web"],
  "compilerOptions": {
    "strict": true,
    "jsx": "react-jsx"
  },
  "fmt": {
    "useTabs": false,
    "lineWidth": 100
  }
}

Each workspace can have its own deno.json for package-specific configuration, imports, and exports. Cross-workspace imports use the workspace name:

// packages/api/src/server.ts
import { validateUser } from "@myapp/core";  // resolves to packages/core
import { renderPage } from "@myapp/web";      // resolves to packages/web

The workspace resolution is compatible with TypeScript's project references and works with deno task for running scripts across packages. deno task --filter "packages/*" runs a task in all matching workspaces — similar to Turborepo's turbo run but built into the runtime.

This was one of the most requested features for teams considering Deno for production monorepos. Without workspaces, managing shared code across multiple Deno projects required URL imports or manual symlinks.

npm Compatibility: The 97% Milestone

Deno 3 reaches 97% compatibility with the top 1000 npm packages (up from ~85% in Deno 2). The remaining 3% are packages that rely on Node.js-specific internals (vm.runInContext edge cases, certain child_process.fork patterns, worker_threads with shared memory).

The key improvements:

Native addon support. Deno 3 can load .node files (C/C++ native addons) through an FFI bridge. This unblocks packages like bcrypt, sharp, canvas, sqlite3, and better-sqlite3 that were previously incompatible. The bridge adds ~2-5ms overhead per native call compared to direct Node.js binding, which is negligible for most use cases.

Full package.json exports map. Deno 2 supported main and basic exports in package.json. Deno 3 handles the full exports map spec: conditional exports (import/require/node/deno/default), subpath exports, and subpath patterns. This fixes compatibility with packages like @radix-ui/*, @mantine/*, and msw that use complex export maps.

deno install command. Creates a node_modules directory and deno.lock lockfile from package.json or deno.json dependencies:

# From package.json
deno install

# From deno.json imports
deno install --lock

# Add a specific package
deno install npm:express@5

The generated node_modules follows the standard flat structure that Node.js uses, so tools that read node_modules directly (bundlers, linters, IDE resolvers) work without configuration.

Performance Improvements

Deno 3's performance headline is the cold start optimization. The V8 snapshot system was rebuilt to include more of Deno's built-in APIs in the startup snapshot, reducing cold start time from ~45ms to ~28ms. This matters for serverless deployments (Deno Deploy, AWS Lambda) where cold starts are user-facing latency.

HTTP server throughput also improved. The Deno.serve() API — Deno's built-in HTTP server — now handles ~165K requests/second for a simple JSON response on a single core (up from ~140K in Deno 2). This puts it ahead of Node.js's http module (~95K req/s) and competitive with Bun's HTTP server (~180K req/s).

For a full runtime performance comparison, see Bun 2 vs Node.js 24 vs Deno 3.

// Deno 3 HTTP server — the standard pattern
Deno.serve({ port: 3000 }, async (req) => {
  const url = new URL(req.url);

  if (url.pathname === "/api/health") {
    return Response.json({ status: "ok" });
  }

  if (url.pathname === "/api/users" && req.method === "POST") {
    const body = await req.json();
    const user = await db.users.create(body);
    return Response.json(user, { status: 201 });
  }

  return new Response("Not Found", { status: 404 });
});

Security Model Updates

Deno's permission system remains opt-in, but Deno 3 adds permission groups for common patterns:

# Individual permissions (unchanged)
deno run --allow-net --allow-read=./data --allow-env=DATABASE_URL server.ts

# Permission group: web server
deno run --allow-server server.ts
# Equivalent to: --allow-net --allow-read --allow-env

# Permission group: development
deno run --allow-dev test.ts
# Equivalent to: --allow-all (but only in dev environments)

The --allow-server group is the pragmatic addition. Most server applications need network access, file reading, and environment variables. Rather than listing all three on every run command, the group covers the common case. The --deny-* flags still work for fine-grained restrictions within a group:

# Allow server permissions but deny write access
deno run --allow-server --deny-write server.ts

Migrating a Node.js Project

For an Express or Fastify application, the migration path is:

Step 1: Add a deno.json:

{
  "nodeModulesDir": "auto",
  "compilerOptions": {
    "allowImportingTsExtensions": true
  }
}

Step 2: Run deno install to generate node_modules and the lockfile from your existing package.json.

Step 3: Replace Node.js-specific imports. Most work without changes, but some patterns need updates:

// Before (Node.js)
import path from 'path';
import { readFile } from 'fs/promises';

// After (Deno — both work, but Deno-native is preferred)
import path from 'node:path';          // node: prefix works in Deno 3
import { readFile } from 'node:fs/promises';  // same API

// Or use Deno APIs directly
const data = await Deno.readTextFile('./config.json');

Step 4: Update package.json scripts to use deno commands:

{
  "scripts": {
    "dev": "deno run --allow-server --watch src/server.ts",
    "build": "deno compile --output=./dist/server src/server.ts",
    "test": "deno test src/"
  }
}

Step 5: Handle edge cases. The most common issues:

  • __dirname / __filename — Use import.meta.dirname and import.meta.filename (added in Deno 2, stable in Deno 3)
  • require() — Works in .cjs files and npm packages; for .ts files, use import
  • Global process — Available when running with --allow-env; or import from node:process

For a broader comparison of the JavaScript runtime ecosystem, see Bun vs Node.js vs Deno.

Deploy and Compile

Deno 3 improves both deployment paths:

Deno Deploy (edge serverless) now supports npm packages that use native addons, expanding the range of deployable applications. The deploy command is unchanged:

deployctl deploy --project=my-api src/server.ts

deno compile produces a single executable binary (no runtime dependency) that includes your application, its dependencies, and a stripped-down Deno runtime. Deno 3 reduces compiled binary sizes by 15-20% through better tree-shaking of unused runtime APIs. A simple HTTP server compiles to ~45 MB (down from ~55 MB in Deno 2).

# Compile for current platform
deno compile --allow-server --output=./server src/server.ts

# Cross-compile
deno compile --target=x86_64-unknown-linux-gnu --output=./server-linux src/server.ts

The compiled binary is fully self-contained — useful for Docker images (no runtime installation needed) and CLI tool distribution.

Should You Switch?

Switch if: You're starting a new project, you want built-in TypeScript without configuration, you value the security model, or you're deploying to Deno Deploy / edge runtimes.

Stay on Node.js if: You depend on packages in the 3% incompatible list, your team's tooling is deeply Node-specific (nvm, npm workspaces with lifecycle scripts), or your deployment pipeline assumes Node.js.

Use both if: Your monorepo has Deno services and npm-based frontends. Deno 3's workspace support makes this viable without separate toolchains.

Deno 3 is the release where "will my npm packages work?" stops being the first question. For most applications, the answer is yes. The remaining question is whether Deno's defaults (TypeScript, permissions, standard library) provide enough value over Node.js's ecosystem depth. For many teams in 2026, they do.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.