Reproduction
https://github.com/adamhake/netlify-node-middleware-esm-repro
npm install
npx netlify build --offline
npx netlify serve --offline --port 8899
curl -i http://localhost:8899/protected # 500, "nextHandler is not a function"
curl -i http://localhost:8899/ # 200 (not in the matcher)
Control in the same repo: node scripts/set-case.mjs cjs swaps the ESM external (nanoid) for a CJS one (ms). Nothing else changes. /protected then returns 200.
next dev and next start both work. The build succeeds. Failure is only when the Node.js middleware runs as a Netlify edge function.
Reproduces on turbopack (Next 16 default) and webpack.
Expected
Node.js middleware (src/proxy.ts) that imports an ESM-only package listed in serverExternalPackages should run on Netlify the same way it does with next start.
Actual
Matched routes return HTTP 500 nextHandler is not a function.
Unhandled Rejection: Error: Failed to load external module nanoid-782ff4eee3a5ce9a:
TypeError: Import "nanoid-782ff4eee3a5ce9a" not a dependency and not in import map
from ".../server/.next/server/chunks/[turbopack]_runtime.js"
[___netlify-edge-handler-node-middleware] TypeError: nextHandler is not a function
at handleMiddleware (edge-runtime/middleware.ts:63:26)
The edge function still loads after that rejection (Loaded edge function Next.js Middleware Handler); the default export is just not a function.
Environment
next 16.3.0
@netlify/plugin-nextjs 5.15.13 (latest on npm as of 2026-08-21; also present on main)
netlify-cli 26.1.0, Node 22.22.2, Deno edge bootstrap v2-17-1
- ESM package:
nanoid@5.1.16 ("type": "module")
- CJS control:
ms@2.1.3
Root cause
serverExternalPackages makes Next emit a runtime import() of a bare specifier (turbopack hashes it: nanoid-782ff4eee3a5ce9a; webpack uses nanoid). Node.js middleware is wrapped as a Deno edge function whose import map does not include that specifier.
copyHandlerDependenciesForNodeMiddleware does inline the package as virtual CJS modules and registers the hashed specifier as a virtual CJS symlink (nanoid-782ff4eee3a5ce9a → ../../node_modules/nanoid). That registry is only reachable through the patched require() in edge-runtime/lib/cjs.ts. An ESM external never goes through require().
deno_import_map is not a workaround: with webpack it can clear the import-map error, but the route still 500s; with turbopack the specifier is a build-dependent hash.
Unreleased 5.16.0 (#3554) only awaits the generated require(). It does not add import-map entries and does not change cjs.ts.
Suggested fix
Either add traced ESM externals to the edge function import map, or fail the build when middleware reaches an ESM package in serverExternalPackages — the same treatment .node addons already get in copyHandlerDependenciesForNodeMiddleware.
Leaving the package out of serverExternalPackages lets Next bundle it and avoids the 500 for packages like nanoid with no native code. That is not viable when the package must be external.
Related
Reproduction
https://github.com/adamhake/netlify-node-middleware-esm-repro
Control in the same repo:
node scripts/set-case.mjs cjsswaps the ESM external (nanoid) for a CJS one (ms). Nothing else changes./protectedthen returns 200.next devandnext startboth work. The build succeeds. Failure is only when the Node.js middleware runs as a Netlify edge function.Reproduces on turbopack (Next 16 default) and webpack.
Expected
Node.js middleware (
src/proxy.ts) that imports an ESM-only package listed inserverExternalPackagesshould run on Netlify the same way it does withnext start.Actual
Matched routes return HTTP 500
nextHandler is not a function.The edge function still loads after that rejection (
Loaded edge function Next.js Middleware Handler); the default export is just not a function.Environment
next16.3.0@netlify/plugin-nextjs5.15.13 (latest on npm as of 2026-08-21; also present onmain)netlify-cli26.1.0, Node 22.22.2, Deno edge bootstrap v2-17-1nanoid@5.1.16("type": "module")ms@2.1.3Root cause
serverExternalPackagesmakes Next emit a runtimeimport()of a bare specifier (turbopack hashes it:nanoid-782ff4eee3a5ce9a; webpack usesnanoid). Node.js middleware is wrapped as a Deno edge function whose import map does not include that specifier.copyHandlerDependenciesForNodeMiddlewaredoes inline the package as virtual CJS modules and registers the hashed specifier as a virtual CJS symlink (nanoid-782ff4eee3a5ce9a→../../node_modules/nanoid). That registry is only reachable through the patchedrequire()inedge-runtime/lib/cjs.ts. An ESM external never goes throughrequire().deno_import_mapis not a workaround: with webpack it can clear the import-map error, but the route still 500s; with turbopack the specifier is a build-dependent hash.Unreleased 5.16.0 (#3554) only
awaits the generatedrequire(). It does not add import-map entries and does not changecjs.ts.Suggested fix
Either add traced ESM externals to the edge function import map, or fail the build when middleware reaches an ESM package in
serverExternalPackages— the same treatment.nodeaddons already get incopyHandlerDependenciesForNodeMiddleware.Leaving the package out of
serverExternalPackageslets Next bundle it and avoids the 500 for packages likenanoidwith no native code. That is not viable when the package must be external.Related