Skip to content

Web Development · Runtime

Deno 2 in Production: What Actually Changed and When to Use It

Deno 2 ships with full Node.js compatibility, npm support, and a revised standard library. Here's what that means for teams evaluating it as a serious Node alternative.

Anurag Verma

Anurag Verma

6 min read

Deno 2 in Production: What Actually Changed and When to Use It

Sponsored

Share

Deno spent its first few years being an interesting experiment that most teams couldn’t justify using. The missing npm compatibility was the blocker. Deno 2 removed that blocker. Now the question is whether the rest of what Deno offers is worth changing your tooling for.

The honest answer: it depends on what you’re building and how much friction you tolerate in your current setup. But it’s worth knowing what the 2026 version of Deno looks like, because the project has changed enough that impressions from 2022 or 2023 don’t apply.

What Actually Changed in Deno 2

The original version of Deno required URLs for imports and had no npm support. That was a principled stance, but it meant you couldn’t use most of the JavaScript ecosystem. Deno 2 dropped that stance in favor of being a better Node.

The main additions:

npm compatibility. You can import any npm package using the npm: prefix. Deno fetches and caches it, no node_modules folder required (though you can opt into one for compatibility with tools that expect it).

import express from "npm:express@4";
import { Hono } from "npm:hono";
import postgres from "npm:postgres";

package.json support. Deno 2 reads package.json if it exists, which means most Node.js projects work with minimal changes. Deno also has its own deno.json format with more features.

JSR as the primary registry. Deno maintains JSR (jsr.io) as a TypeScript-native registry. Packages published there include type information and work across Deno, Node, and Bun. The Deno standard library moved off deno.land/std to JSR.

Workspace support. Monorepos work properly with deno.json workspace configuration, bringing Deno in line with npm/pnpm workspaces.

Project Setup

A minimal Deno 2 project needs a deno.json:

{
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-env main.ts",
    "start": "deno run --allow-net --allow-env main.ts",
    "test": "deno test --allow-net",
    "lint": "deno lint",
    "fmt": "deno fmt"
  },
  "imports": {
    "hono": "npm:hono@^4",
    "@/": "./src/"
  }
}

The imports field defines import maps, letting you use bare specifiers without a build step.

A basic HTTP server with Hono:

// main.ts
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => c.json({ ok: true }));

app.get("/users/:id", async (c) => {
  const id = c.req.param("id");
  return c.json({ id, name: "placeholder" });
});

Deno.serve({ port: 8000 }, app.fetch);

Run it:

deno task dev

No install step. Deno downloads hono on first run, caches it, and runs.

The Permission Model in Practice

The most opinionated part of Deno is permissions. By default, a script can’t read files, make network requests, access environment variables, or run subprocesses without explicit flags.

# Grant specific permissions
deno run --allow-net=api.stripe.com,localhost:8000 --allow-env=STRIPE_KEY main.ts

# Or grant all (for development)
deno run -A main.ts

This matters most for scripts running in CI or as part of an automation pipeline, where you want to know exactly what external access a script has. For web servers, --allow-net and --allow-env are usually all you need.

The permissions model doesn’t add meaningful protection if you’re running deno run -A everywhere, but fine-grained permissions are genuinely useful for scripts and tools.

Built-in Tooling

Deno ships a formatter, linter, and test runner. No config files needed for any of them to work.

deno fmt               # Format all .ts/.tsx/.js files
deno lint              # Lint with sensible defaults
deno test              # Run all files matching *_test.ts or *.test.ts
deno test --coverage   # Generate coverage report

The test runner uses standard assertions from the Deno standard library:

// user_test.ts
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { createUser, getUser } from "./user.ts";

Deno.test("createUser returns a user with the right email", async () => {
  const user = await createUser({ email: "test@example.com" });
  assertEquals(user.email, "test@example.com");
});

Deno.test("getUser throws for unknown id", async () => {
  await assertRejects(() => getUser("nonexistent"), Error, "not found");
});

No Jest, no Vitest, no config. The built-in runner supports async tests, test isolation, and concurrent test execution.

Deploying Deno

Deno Deploy is the managed platform, similar to Vercel edge functions. You push code and it runs at the edge in 35+ regions. It’s free for small workloads and priced by requests and egress at scale.

Deno Deploy handles the permission model automatically: each deployment declares what permissions it needs and the platform enforces them.

For self-hosted deployments, Deno compiles to a single binary:

deno compile --output app --allow-net --allow-env main.ts
./app

The output is a self-contained executable with the Deno runtime embedded. Useful for distribution or for environments where you don’t want to install Deno separately.

Docker with Deno is straightforward:

FROM denoland/deno:2.3.3
WORKDIR /app
COPY deno.json .
COPY . .
RUN deno cache main.ts
CMD ["deno", "task", "start"]

The cache step downloads dependencies at image build time, so the container doesn’t make network requests on startup.

Connecting to Databases

Most Node database libraries work via npm: imports. Deno also has JSR-native alternatives that are worth knowing:

// Postgres via npm
import postgres from "npm:postgres";
const sql = postgres(Deno.env.get("DATABASE_URL")!);

// Or via JSR
import { Pool } from "jsr:@db/postgres";
const pool = new Pool(Deno.env.get("DATABASE_URL")!, 3);

Prisma works with Deno via its edge client. Drizzle ORM has native Deno support. For SQLite, Deno ships a built-in Deno.openKv() for simple key-value storage, plus access to SQLite through the @db/sqlite JSR package.

Deno vs Node: When to Actually Switch

The case for Deno is strongest in specific scenarios:

New scripts and automation. For CLI tools and scripts, Deno’s single-file execution with no install step and built-in TypeScript support beats Node. No tsconfig.json, no ts-node, no package.json. Just write TypeScript and run it.

Security-sensitive workloads. Permission model lets you run untrusted code with explicit constraints.

Edge functions. Deno Deploy is a good alternative to Cloudflare Workers and Vercel edge functions, with strong TypeScript support and a familiar API surface.

New greenfield services. No node_modules means less disk I/O and faster container starts. The built-in tooling reduces setup time.

The case against switching: if you have an existing Node codebase that works, migration costs are real. Some npm packages that rely on Node internals (child_process, native modules) need compatibility shims. The Deno ecosystem, while growing, has fewer production case studies than Node.

The Practical Verdict

Deno 2 closed the compatibility gap that previously made it hard to recommend. For new services, especially those targeting edge environments, it’s worth evaluating seriously. The built-in TypeScript support and zero-config tooling reduce setup friction, and the permission model is a genuine security improvement for scripts.

For teams already running Node, the migration justification needs to be concrete: “we want edge deployment” or “we’re writing a lot of CLI tools and the built-in tooling reduces boilerplate.” Switching Node for Deno because Deno seems more modern is not a good reason.

Start with a new service or tool. If the development experience feels better, expand from there.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored