Deno 2.0: Compatible with Node (Finally)

dev javascript

Deno 2.0 was released in October 2024. The biggest change: full Node.js compatibility. You can now run most npm packages without modification. This removes the primary barrier to Deno adoption.

What’s New in 2.0

Node Compatibility

// This now works in Deno 2.0
import express from "npm:express";

const app = express();

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

app.listen(3000);

npm packages just work.

package.json Support

{
    "name": "my-deno-app",
    "dependencies": {
        "express": "^4.18.0",
        "lodash": "^4.17.0"
    }
}
// Import from package.json
import express from "express";
import _ from "lodash";

node_modules

# Optionally use node_modules
deno install

# Directory structure works like Node
node_modules/
├── express/
├── lodash/
└── ...

Migration from Node

Before (Node.js)

// server.js
const express = require('express');
const { Pool } = require('pg');

const pool = new Pool({
    connectionString: process.env.DATABASE_URL
});

const app = express();
app.get('/users', async (req, res) => {
    const result = await pool.query('SELECT * FROM users');
    res.json(result.rows);
});

app.listen(3000);

After (Deno 2.0)

// server.ts
import express from "npm:express";
import pg from "npm:pg";

const pool = new pg.Pool({
    connectionString: Deno.env.get("DATABASE_URL")
});

const app = express();
app.get('/users', async (req, res) => {
    const result = await pool.query('SELECT * FROM users');
    res.json(result.rows);
});

app.listen(3000);

Minimal changes required.

Why Deno Now

Security by Default

# Explicit permissions required
deno run --allow-net --allow-read server.ts

# Or grant all (like Node)
deno run -A server.ts

TypeScript Native

// No tsconfig, no transpilation step
interface User {
    id: number;
    name: string;
}

function greet(user: User): string {
    return `Hello, ${user.name}!`;
}

Just run .ts files directly.

Built-in Tooling

deno fmt          # Format code
deno lint         # Lint code
deno test         # Run tests
deno bench        # Benchmarks
deno compile      # Create executable
deno doc          # Generate docs

No additional dev dependencies.

Standard Library

import { serve } from "https://deno.land/std/http/server.ts";
import { join } from "https://deno.land/std/path/mod.ts";
import { parse } from "https://deno.land/std/flags/mod.ts";

// Stable, versioned, audited

Comparison

FeatureNode.jsDeno 2.0
npm packagesNative✅ Full support
TypeScriptRequires setupNative
SecurityAll permissionsOpt-in
Built-in toolsSeparateIncluded
URL importsNoYes
package.jsonRequiredOptional

Deno Deploy

Serverless edge deployment:

// Simple edge function
Deno.serve((req) => {
    return new Response("Hello from the edge!");
});
# Deploy
deno deploy

Global edge network, automatic HTTPS.

Testing

// user_test.ts
import { assertEquals } from "https://deno.land/std/assert/mod.ts";
import { getUser } from "./user.ts";

Deno.test("getUser returns user by id", async () => {
    const user = await getUser(1);
    assertEquals(user.name, "Alice");
});

Deno.test("getUser throws on invalid id", async () => {
    await assertRejects(
        () => getUser(-1),
        Error,
        "Invalid user ID"
    );
});
deno test

Coverage

deno test --coverage=coverage
deno coverage coverage --lcov

Key Patterns

Fresh (Web Framework)

// routes/index.tsx
import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
    async GET(req, ctx) {
        const data = await fetchData();
        return ctx.render(data);
    },
};

export default function Home({ data }: PageProps) {
    return <div>{data.message}</div>;
}

Islands architecture, no build step.

Oak (Express-like)

import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const router = new Router();
router.get("/", (ctx) => {
    ctx.response.body = "Hello Oak!";
});

const app = new Application();
app.use(router.routes());

await app.listen({ port: 8000 });

Compiling to Executables

# Create standalone executable
deno compile --allow-net --allow-read server.ts

# Output: server (or server.exe on Windows)
./server

Single binary, no runtime needed.

Configuration

// deno.json
{
    "compilerOptions": {
        "strict": true
    },
    "imports": {
        "std/": "https://deno.land/std@0.208.0/"
    },
    "tasks": {
        "dev": "deno run --watch --allow-net server.ts",
        "test": "deno test --allow-read"
    }
}

Should You Switch?

Yes, If

Not Yet, If

Final Thoughts

Deno 2.0 removes the “can’t use npm packages” objection. With full Node compatibility, the choice is now about trade-offs rather than limitations.

For new projects, Deno’s security model and built-in tooling are compelling. For existing Node projects, migration is now actually feasible.


The modern JavaScript runtime grows up.

All posts