Acumatica · Deno

Deno 2 in Production — A Field Guide

How Deno 2's npm compatibility, permissions model, and built-in tooling hold up running real production services, and when the migration from Node is actually worth it.

John Kihiu12 min read

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.

Scope permissions narrowly

--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.

JSON · deno.json
{
  "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.

Not every npm package works cleanly

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 pointDeno 2 fix
No real npm supportnpm: specifiers, node_modules compat, package.json support
No monorepo storyNative workspaces in deno.json
Frequent breaking changesSemver-committed 2.x line, LTS releases
Separate lint/format/test toolingStill 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.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.