Deno Cannot Resolve Node Built-ins After Migrating a Legacy Script
Moving a maintenance script from Node to Deno should have taken an afternoon. It took two days, and nearly every hour went into imports that resolve in one runtime and simply do not exist in the other.
Benjamin Fazli
Principal EngineerSkopje, North Macedonia

The symptom
A scheduled script read files, hashed them, and posted a manifest to an internal API. Under Deno the first line failed:
error: Relative import path "fs" not prefixed with / or ./ or ../The message is accurate and still misleading. It reads like a path problem, so the instinct is to start editing paths. The real issue is that Deno does not treat bare specifiers as Node built-ins unless you tell it to.
The fix
Prefix every Node built-in with node:. Deno then maps it to its compatibility layer:
import { readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { join } from 'node:path'That single change resolved most of the file. Four further things came up, and they are the ones nobody warns you about.
Permissions are explicit. Deno reads nothing and calls nothing until you allow it. Grant narrowly rather than reaching for a blanket flag:
deno run --allow-read=./data --allow-net=api.internal.example.com script.ts`process` exists, but not everywhere. process.env works through the compatibility layer. Deno.env.get() is the native equivalent and is clearer about what it does. Reading environment variables also needs a permission, which catches people out on their first run.
`__dirname` does not exist. In an ES module the equivalent is derived from the module URL:
import { dirname, fromFileUrl } from 'node:path'
const here = dirname(fromFileUrl(import.meta.url))Not every native package has an equivalent. A dependency with a compiled addon will not load. In my case it was a hashing helper, and swapping it for node:crypto made the script both shorter and faster. If the dependency is genuinely irreplaceable, the migration is not worth forcing.
Was it worth it
For this script, yes. No install step, no lockfile, no node_modules, direct TypeScript execution, and permissions I can actually reason about in a scheduled job. For an application with a large dependency tree I would want a proof of concept before committing.
When a migration error mentions paths, check whether the runtime resolves specifiers the way you assume. The error text describes the symptom, and the assumption is the bug.
