When Deno first launched, I was excited but couldn’t justify using it for real projects. The lack of npm compatibility was a dealbreaker — I’m not rewriting my entire dependency stack for a slightly nicer runtime. But Deno 2.0 changed everything. I’ve been using it for three production microservices since its release, and the experience has been genuinely excellent.
Let me walk you through what’s actually new, what works, and whether it’s time for your team to consider Deno seriously.
The Big Picture: What Deno 2.0 Fixed
Deno 1.x had a chicken-and-egg problem: you couldn’t use npm packages, so nobody built for Deno, so there were no packages, so nobody used Deno. Ryan Dahl (Deno’s creator and Node.js’s original author) recognized this and made the pragmatic choice: full Node.js compatibility.
Here’s what Deno 2.0 delivers:
- ✅ Full npm package support (no import maps needed)
- ✅ Node.js built-in module compatibility (
fs,path,crypto, etc.) - ✅
package.jsonsupport - ✅ Backwards-compatible with existing Deno code
- ✅ Improved permission system
- ✅ Long-term support (LTS) releases
- ✅ Workspaces for monorepos
npm Compatibility: It Actually Works
The single biggest change. You can now use npm packages just like you would in Node.js:
// Using npm packages directly - no import maps needed
import express from "npm:express";
import { z } from "npm:zod";
import { PrismaClient } from "npm:@prisma/client";
const app = express();
const prisma = new PrismaClient();
app.get("/users", async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
app.listen(3000);
Or, if you prefer the package.json approach:
{
"dependencies": {
"express": "^4.18.0",
"zod": "^3.22.0",
"@prisma/client": "^5.0.0"
}
}
// With package.json, imports work exactly like Node.js
import express from "express";
import { z } from "zod";
Pro Tip: You can mix Deno’s URL imports and npm imports in the same project. Use URL imports for Deno-native libraries (which are often better designed) and npm imports for ecosystem packages that don’t have Deno equivalents.
Compatibility Numbers
In my testing across our monorepo (312 npm packages):
- Works perfectly: 296 packages (95%)
- Minor adjustments needed: 11 packages (3.5%)
- Doesn’t work: 5 packages (1.5%) — all native C++ addons
This is a massive improvement from Deno 1.x where I’d estimate only 40-50% of npm worked reliably.
The Permission System: Security Without the Pain
Deno’s permission system was always its strongest differentiator, but in 1.x it was annoying in practice. Deno 2.0 makes it actually usable:
# Old way (Deno 1.x) - specify everything upfront
deno run --allow-net --allow-read=./config --allow-env=DATABASE_URL server.ts
# New way (Deno 2.0) - interactive prompts during development
deno run server.ts
# ⚠️ Deno requests net access to "localhost:5432". Allow? [y/n/A]
# ⚠️ Deno requests read access to "./config". Allow? [y/n/A]
Permission Configuration in deno.json
{
"permissions": {
"net": ["localhost", "api.stripe.com", "database:5432"],
"read": ["./src", "./config", "./public"],
"write": ["./logs", "./tmp"],
"env": ["DATABASE_URL", "STRIPE_KEY", "NODE_ENV"],
"run": ["prisma", "curl"]
}
}
This is genuinely valuable for security. In my team, we caught a compromised npm package that was trying to make network requests to an unknown endpoint — Deno’s permission system blocked it and alerted us. With Node.js, it would have silently exfiltrated data.
Pro Tip: For production, always use explicit permissions in your deno.json rather than
--allow-all. Set up your CI to run with the same permissions as production — this catches permission issues before deployment.
Built-in Toolchain
Like Bun, Deno ships with batteries included. But Deno’s toolchain is arguably more mature:
| Tool | Node.js Equivalent | Deno Built-in |
|---|---|---|
| Formatter | Prettier | deno fmt |
| Linter | ESLint | deno lint |
| Test runner | Jest/Vitest | deno test |
| Type checker | tsc | deno check |
| Bundler | esbuild/webpack | deno compile |
| Documentation | TypeDoc | deno doc |
| Benchmarking | custom | deno bench |
| Task runner | npm scripts | deno task |
Formatting and Linting
# Format entire project
deno fmt
# Lint with sane defaults
deno lint
# Type check without running
deno check src/server.ts
The formatter is opinionated (like Prettier) but configurable in deno.json:
{
"fmt": {
"options": {
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": true,
"semicolons": true
}
}
}
Testing: First-Class and Fast
Deno’s test runner is excellent. It supports:
- BDD-style
describe/itblocks - Snapshot testing
- Mocking and spies
- Parallel test execution
- Coverage reporting
// user.test.ts
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { describe, it, beforeEach } from "jsr:@std/testing/bdd";
import { stub } from "jsr:@std/testing/mock";
import { UserService } from "./user.service.ts";
describe("UserService", () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it("should create a user with valid data", async () => {
const user = await service.create({
name: "Michael",
email: "[email protected]",
});
assertEquals(user.name, "Michael");
assertEquals(typeof user.id, "string");
});
it("should reject duplicate emails", async () => {
await service.create({ name: "First", email: "[email protected]" });
await assertRejects(
() => service.create({ name: "Second", email: "[email protected]" }),
Error,
"Email already exists"
);
});
});
# Run tests
deno test
# With coverage
deno test --coverage=coverage/
# Generate HTML report
deno coverage coverage/ --html
Benchmark Support
// bench.ts
Deno.bench("JSON parse", () => {
JSON.parse('{"name": "Michael", "age": 30}');
});
Deno.bench("URL parse", () => {
new URL("https://devtools.day/blog/deno-2-features/");
});
$ deno bench
benchmark time (avg) iter/s
JSON parse 142.3 ns/iter 7,027,400
URL parse 891.2 ns/iter 1,122,100
JSR: The JavaScript Registry
Deno introduced JSR (JavaScript Standard Registry) as a modern replacement for npm. It’s TypeScript-first and works with any runtime:
// JSR packages use the jsr: specifier
import { parse } from "jsr:@std/yaml";
import { serve } from "jsr:@std/http";
import { z } from "jsr:@zod/zod";
Why JSR Matters
| Feature | npm | JSR |
|---|---|---|
| TypeScript support | Requires compilation | Native .ts publishing |
| Type checking | Separate @types packages | Built-in |
| Module system | CJS + ESM mess | ESM only |
| Documentation | Manual READMEs | Auto-generated from JSDoc |
| Provenance | Optional | Built-in |
| Works with Node.js | ✅ | ✅ |
| Works with Deno | Via npm: specifier | ✅ Native |
| Works with Bun | ✅ | ✅ |
Pro Tip: When publishing libraries, consider publishing to both npm and JSR. The
jsr publishcommand is incredibly simple, and it auto-generates the documentation from your TypeScript types and JSDoc comments.
Workspaces: Monorepo Support
Deno 2.0 finally supports workspaces, making monorepo setups practical:
// deno.json (root)
{
"workspace": [
"./packages/shared",
"./packages/api",
"./packages/web",
"./packages/cli"
]
}
// packages/api/deno.json
{
"name": "@myapp/api",
"version": "1.0.0",
"exports": "./mod.ts",
"imports": {
"@myapp/shared": "../shared/mod.ts"
}
}
For teams considering monorepo architectures, Deno’s workspace support is cleaner than npm/pnpm workspaces because there’s no hoisting confusion or phantom dependency issues.
Deno Deploy: Serverless at the Edge
Deno Deploy is Deno’s serverless platform, running your code at 35+ edge locations globally:
// server.ts — deploys to Deno Deploy as-is
import { serve } from "jsr:@std/http";
serve((req: Request) => {
const url = new URL(req.url);
if (url.pathname === "/api/hello") {
return Response.json({
message: "Hello from the edge!",
region: Deno.env.get("DENO_REGION")
});
}
return new Response("Not Found", { status: 404 });
});
Deploy Performance
| Metric | Deno Deploy | AWS Lambda | Cloudflare Workers |
|---|---|---|---|
| Cold start | ~10ms | ~200-800ms | ~5ms |
| Regions | 35+ | 25+ | 300+ |
| Max execution | 30s | 15min | 30s (free) |
| Free tier | 100K req/day | 1M req/month | 100K req/day |
Standard Library: @std
Deno’s standard library is curated, well-tested, and audited — unlike the Wild West of npm where you need a package to left-pad a string:
import { parse as parseYaml } from "jsr:@std/yaml";
import { join, resolve } from "jsr:@std/path";
import { crypto } from "jsr:@std/crypto";
import { delay } from "jsr:@std/async";
import { format } from "jsr:@std/datetime";
import { encode as base64Encode } from "jsr:@std/encoding/base64";
// These are maintained by the Deno team with proper security reviews
const config = parseYaml(await Deno.readTextFile("./config.yaml"));
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode("data"));
Common Mistakes When Adopting Deno
1. Trying to Port Everything at Once
Start with a single microservice or CLI tool. Don’t attempt to migrate your entire monorepo in one sprint.
2. Fighting the Permission System
Instead of using --allow-all everywhere, invest 10 minutes to configure proper permissions. Future you will thank present you when a supply chain attack gets blocked.
3. Ignoring deno.json Configuration
// deno.json - configure once, never fight with tooling again
{
"compilerOptions": {
"strict": true,
"lib": ["deno.window", "dom"]
},
"imports": {
"@std/": "jsr:@std/",
"~/": "./src/"
},
"tasks": {
"dev": "deno run --watch --allow-all src/main.ts",
"test": "deno test --allow-all",
"check": "deno fmt --check && deno lint && deno check src/"
}
}
4. Not Using Import Maps
Import maps eliminate repetitive specifiers:
// deno.json imports section
{
"imports": {
"express": "npm:express@^4.18",
"zod": "npm:zod@^3.22",
"@/": "./src/"
}
}
5. Comparing Raw Speed Instead of Total DX
Deno might be slightly slower than Bun in raw benchmarks, but the integrated toolchain (fmt + lint + test + check) and security model often make it the more productive choice for teams that value code quality and security.
Deno vs Node.js vs Bun: When to Choose Each
| Factor | Choose Node.js | Choose Deno | Choose Bun |
|---|---|---|---|
| Ecosystem maturity | ✅ Best | Good | Good |
| Security model | Basic | ✅ Best | Basic |
| Raw performance | Good | Good | ✅ Best |
| Built-in toolchain | ❌ Minimal | ✅ Complete | ✅ Good |
| Enterprise adoption | ✅ Dominant | Growing | Growing |
| Serverless/Edge | Good | ✅ Best (Deploy) | Good |
| Learning resources | ✅ Abundant | Moderate | Growing |
| TypeScript DX | Requires setup | ✅ Native | ✅ Native |
My Production Setup with Deno
Here’s how I structure a Deno production service:
my-service/
├── deno.json
├── deno.lock
├── src/
│ ├── main.ts
│ ├── routes/
│ ├── services/
│ ├── middleware/
│ └── lib/
├── tests/
│ ├── unit/
│ └── integration/
└── scripts/
├── migrate.ts
└── seed.ts
// deno.json
{
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read=. src/main.ts",
"start": "deno run --allow-net --allow-env --allow-read=. src/main.ts",
"test": "deno test --allow-all tests/",
"check": "deno check src/main.ts",
"lint": "deno lint && deno fmt --check"
},
"imports": {
"hono": "jsr:@hono/hono",
"@std/": "jsr:@std/",
"drizzle-orm": "npm:drizzle-orm",
"postgres": "npm:postgres"
}
}
If you’re also setting up proper testing strategies, Deno’s built-in test runner makes it trivial to maintain comprehensive test coverage without dependency management overhead.
FAQ
Is Deno ready for enterprise production use?
Yes, but evaluate based on your specific needs. Deno 2.0 offers LTS releases, the npm compatibility story is solid, and companies like Netlify, Slack, and Shopify use it in production. The main consideration is team familiarity — if your team has deep Node.js expertise, factor in the learning curve for Deno’s permission system and toolchain differences.
Can I gradually migrate from Node.js to Deno?
Absolutely. Deno 2.0’s Node.js compatibility mode means you can often run existing Node.js code with minimal changes. Start with a new microservice or internal tool in Deno. As your team gains confidence, migrate additional services. You can even run Deno and Node.js services side-by-side in the same infrastructure.
How does Deno handle environment variables?
Deno requires explicit permission to read environment variables: --allow-env or --allow-env=SPECIFIC_VAR. It natively reads .env files without any package. Use Deno.env.get("DATABASE_URL") to read variables. In deno.json, configure which env vars your app needs so the permission model is documented.
Is Deno faster than Node.js?
In specific benchmarks (HTTP handling, startup time), Deno is 10-30% faster than Node.js. However, for real-world applications with database I/O, the difference is often negligible. Where Deno genuinely saves time is developer experience — no config, instant TypeScript, built-in tools. Choose Deno for the DX, not raw speed.
What about Deno’s standard library vs npm packages?
Use Deno’s @std library whenever possible — it’s audited, well-maintained, and designed for Deno’s permission model. For domain-specific needs (ORMs, framework, cloud SDKs), use npm packages. The best practice is: @std for utilities and fundamentals, npm for specialized functionality.
