Deno 2 dropped the parts of Deno 1 that made teams hesitate — npm compatibility that actually works, a package.json escape hatch, and a slower-but-saner approach to breaking changes — without giving up the things that made Deno worth trying in the first place: TypeScript with no build step, a permissions model that is on by default, and a single binary with a formatter, linter, and test runner built in. Running it in production for the last year has been mostly uneventful, which for a runtime is the highest compliment I can give it.
npm and Node compatibility
The single biggest adoption blocker for Deno 1 was the module system: URL imports were elegant for toy scripts and painful the moment you needed a real npm package. Deno 2 fixes this properly. You can deno add npm:express or deno add npm:react and it resolves, caches, and type-checks the package the same way npm or pnpm would, backed by a deno.json that plays the role package.json used to play alone. Node's node_modules resolution, CommonJS interop, and even package.json itself are all supported now — Deno will read an existing package.json if one is present, which makes migrating an existing Node service far less of a rewrite than it used to be.
Permissions stay on by default
What has not changed, and is still the feature I care about most, is the permissions model. A Deno process has no filesystem, network, or environment access unless you grant it explicitly with flags like --allow-net, --allow-read, or --allow-env. For a service that pulls in third-party dependencies — which in Deno 2 now means the entire npm ecosystem — that boundary is worth more than it was when the ecosystem was smaller. A compromised transitive dependency in a Node app can exfiltrate environment variables silently; in Deno, it needs a permission grant that shows up in your run command or deploy config, which is at minimum a code-review flag.
--allow-net with no argument grants access to any host. In production, scope it: --allow-net=api.stripe.com,db.internal restricts outbound connections to exactly what the service needs, so a compromised dependency can't quietly phone home somewhere else.
Workspaces for monorepos
Deno 2 added first-class workspace support in deno.json, letting you split a project into multiple packages that reference each other locally without publishing to a registry first — the same shape as npm/pnpm/yarn workspaces. This matters for anyone running Deno beyond a single script: a shared types package, an internal SDK, and two or three services can live in one repo with `deno task` running the right command per package, and `deno.lock` giving you a single reproducible lockfile across the whole workspace.
{
"workspace": ["./api", "./worker", "./shared"],
"tasks": {
"dev": "deno run --watch --allow-net --allow-env api/main.ts",
"test": "deno test --allow-net --allow-env",
"check": "deno check **/*.ts"
},
"imports": {
"@shared/": "./shared/src/"
}
}
What production actually looks like
In practice a Deno 2 service in production is a single compiled or JIT-run binary with no node_modules directory to manage, a lockfile committed to the repo, and a small, explicit set of permission flags in the process manager or Dockerfile. Cold starts are fast because there's no dependency install step at container boot — everything is resolved and cached at build time via deno cache or baked into the image. The built-in test runner (deno test), formatter (deno fmt), and linter (deno lint) mean CI configuration is a fraction of the size of an equivalent Node pipeline: no separate Jest, Prettier, and ESLint installs and version-matrix headaches.
Packages that rely on native Node internals, build-time codegen, or deep filesystem assumptions can still misbehave under Deno's compatibility layer. Before committing to a migration, run your actual dependency tree through deno info and smoke-test the packages you rely on most — don't assume 100% compatibility from the marketing copy.
When the migration is worth it
Deno 2 is an easy choice for new services, especially anything TypeScript-heavy where you were previously paying a ts-node or build-step tax. For an existing large Node codebase, the calculus is less clean-cut — the compatibility layer is good, not perfect, and a big migration is real work for a runtime swap that mostly buys you simpler tooling and a tighter security boundary. The teams getting the most value are the ones running many small services, where the built-in tooling and permission model compound across dozens of deployments instead of one.
| Deno 1 pain point | Deno 2 fix |
|---|---|
| No real npm support | npm: specifiers, node_modules compat, package.json support |
| No monorepo story | Native workspaces in deno.json |
| Frequent breaking changes | Semver-committed 2.x line, LTS releases |
| Separate lint/format/test tooling | Still built in, unchanged and stable |
Wrapping up
Deno 2's real achievement is boring: it kept the parts of Deno that were genuinely better than Node — permissions, built-in tooling, TypeScript-first — and removed the friction that kept teams from adopting it. If your evaluation of Deno 1 stalled on "we can't get our npm packages to work," it's worth another look.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.