Published on

Deno 2 for Backend Engineers — The Node.js Alternative That Finally Has npm Support

Authors

Introduction

Deno has long been the JavaScript runtime with the "right" architecture—no node_modules, modern standards-based APIs, TypeScript by default. Deno 2 finally shipped npm compatibility, closing the ecosystem gap. For teams tired of Node.js complexity, Deno 2 is now a viable alternative. This guide covers what changed, production deployment, and when Deno beats Node.js.

What Changed in Deno 2

Deno 2 released in September 2024 with three major shifts:

Native npm Support: Install npm packages without deno.land/x conversion:

// deno.json
{
  "imports": {
    "express": "npm:express@4.18.0",
    "lodash": "npm:lodash@4.17.21"
  }
}

Workspaces: Monorepo support with shared configuration:

// deno.json (root)
{
  "workspace": ["./services/api", "./services/worker"],
  "imports": {
    "shared": "file:./packages/shared"
  }
}

Improved Node Compatibility: The node: protocol works reliably:

import * as fs from 'node:fs'
import * as path from 'node:path'

const data = fs.readFileSync('file.txt', 'utf-8')
const dir = path.dirname(data)

Combined, these changes make Deno a drop-in replacement for Node.js projects.

Deno's HTTP Server with deno serve

Deno ships with deno serve for HTTP:

// server.ts
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url)

    if (url.pathname === '/') {
      return new Response('Hello Deno')
    }

    if (url.pathname === '/api/users') {
      const users = [{ id: 1, name: 'Alice' }]
      return Response.json(users)
    }

    return new Response('Not Found', { status: 404 })
  },
  port: 3000,
}

Run with:

deno serve server.ts

No framework boilerplate. Just a fetch handler. This is faster and simpler than Express.

Built-In TypeScript (No Config)

Deno compiles TypeScript automatically:

// No tsconfig.json needed
import { serve } from 'https://deno.land/std@0.201.0/http/server.ts'

interface User {
  id: number
  name: string
}

const users: User[] = [
  { id: 1, name: 'Alice' },
]

serve((req) => {
  if (req.url === '/api/users') {
    return Response.json(users)
  }
  return new Response('404', { status: 404 })
}, { port: 3000 })

Types flow through without configuration. Compare to Node.js setup with ts-node or tsx.

Deno KV for Edge Storage

Deno Deploy includes KV storage (like Redis):

const kv = await Deno.openKv()

// Set
await kv.set(['users', '1'], { name: 'Alice', email: 'alice@example.com' })

// Get
const user = await kv.get(['users', '1'])
console.log(user.value)

// Atomic operations
const result = await kv.atomic()
  .set(['counter'], 0)
  .set(['users', '1'], user)
  .commit()

KV is transactional and replicated globally. Perfect for edge-deployed APIs.

Permissions Model for Security

Deno runs code in a sandbox by default. Explicit permissions prevent supply-chain attacks:

# Run with no permissions (very restricted)
deno run app.ts

# Grant network access
deno run --allow-net app.ts

# Grant file system access
deno run --allow-read=. --allow-write=. app.ts

# Grant environment variable access
deno run --allow-env app.ts

# Grant all (equivalent to Node.js default)
deno run --allow-all app.ts

A malicious dependency cannot read files or access the network without explicit permission. Node.js has no equivalent.

JSR vs npm

Deno introduces JSR (JavaScript Registry) as an npm alternative:

// Use npm package
import express from 'npm:express@4'

// Use JSR package (newer ecosystem)
import { parseArgs } from 'jsr:@std/cli'

// Mix both
import express from 'npm:express@4'
import { assertEquals } from 'jsr:@std/assert'

JSR packages are published to jsr.io. They ship with TypeScript source, guaranteeing type safety. npm packages are JavaScript-first.

Deno Deploy for Edge Computing

Deploy to Deno Deploy with zero configuration:

# deno.json
{
  "name": "@user/my-api",
  "version": "1.0.0",
  "exports": "./server.ts"
}
deno deploy --prod server.ts

Your API runs on 25+ edge locations globally. Sub-50ms latency everywhere.

deno compile for Single-Binary Deployment

Compile your app into a standalone binary:

deno compile --allow-net --output=my-api server.ts

# Run without Deno installed
./my-api

Single binary, no runtime dependencies. Ship to any server.

Migration From Node.js

Step 1: Create deno.json

{
  "imports": {
    "express": "npm:express@4.18.0",
    "pino": "npm:pino@8.14.0"
  },
  "tasks": {
    "dev": "deno run --allow-net --allow-env server.ts",
    "test": "deno test --allow-all"
  }
}

Step 2: Update imports

// Node.js
import express from 'express'
import * as fs from 'fs'

// Deno
import express from 'npm:express'
import * as fs from 'node:fs'  // or use Deno.open()

Step 3: Replace Node APIs with Deno equivalents (optional)

// Node.js
import * as fs from 'fs'
fs.readFileSync('file.txt')

// Deno
const data = await Deno.readTextFile('file.txt')

Deno''s APIs are simpler and async-first. No callback hell.

Step 4: Test

deno run --allow-net --allow-env server.ts

Most Express/Fastify apps run unchanged.

When Deno Beats Node.js

Deno wins for:

  • Greenfield projects (no legacy Node.js code)
  • Edge computing (Deno Deploy is excellent)
  • Security-sensitive applications (permissions model)
  • Single-binary deployment (deno compile)
  • TypeScript-first teams (no config)

Node.js wins for:

  • Massive npm ecosystem (some packages are Node.js-only)
  • Team familiarity (99% of JavaScript developers know Node.js)
  • Enterprise support (AWS/GCP have no Deno deploy products)

Deno is production-ready. It''s not about being "better" than Node.js—it''s about different trade-offs.

Production Deployment Checklist

For self-hosted Deno:

FROM denoland/deno:debian-1.40.0

WORKDIR /app
COPY . .

RUN deno cache --reload server.ts

EXPOSE 3000
CMD ["deno", "run", "--allow-all", "server.ts"]

For Deno Deploy:

# Install CLI
npm install -g deployctl

# Deploy
deployctl deploy --prod server.ts

Deno Deploy handles HTTPS, global distribution, and auto-scaling.

Deno vs Bun vs Node.js

AspectDenoBunNode.js
Startup100ms<5ms150ms
Throughput25,000 req/s65,000 req/s28,000 req/s
TypeScriptBuilt-inBuilt-inRequires tooling
PermissionsYesNoNo
Edge DeployDeno DeployNoneAWS Lambda
npm compat100% (v2)95%100%
MaturityStableStableVery stable

Choose Deno for security and deployment flexibility. Choose Bun for raw performance. Node.js for ecosystem and team familiarity.

Checklist

  • Create deno.json with npm imports
  • Migrate Express/Fastify app to deno serve
  • Replace Node.js APIs with Deno equivalents
  • Test locally with deno run
  • Add permissions flags (--allow-net, --allow-env)
  • Write tests with Deno''s test runner
  • Containerize or compile to binary
  • Deploy to Deno Deploy or self-hosted
  • Monitor performance and error rates
  • Plan rollback strategy

Conclusion

Deno 2 eliminates the main reason to stay with Node.js—ecosystem lock-in. With full npm support, Deno is now a genuine alternative. For teams building new backends, Deno offers superior architecture (no node_modules), better security (permissions), and exceptional deployment options (Deno Deploy). It''s no longer an experiment; it''s production-ready.