PostgreSQL

drizzle-kit generate: Cannot find module .js under nodenext

Get drizzle-kit generate working in a "type": "module" project that writes .js extensions in its TypeScript imports, without dropping nodenext.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

In a TypeScript project with "module": "nodenext" and "type": "module", schema files import each other with a .js extension, as TypeScript requires: import { users } from './users.js'. drizzle-kit 0.31.9 and older load the schema with a CommonJS require hook that does not map ./users.js to users.ts, so drizzle-kit generate stops with Error: Cannot find module './users.js'. Upgrade drizzle-kit to 0.31.10 or later, where the loader is tsx: in a reproduction, that single change made generate succeed. If you cannot upgrade, run the old CLI under tsx, or point schema at the files tsc emits.

The error from drizzle-kit 0.31.9#

The report is drizzle-orm issue #3348, opened in June 2024 against drizzle-kit 0.22.7 and still open. Commenters hit it again on later releases, with "moduleResolution": "Node16" as well as nodenext. The same failure reproduces on drizzle-kit 0.31.9, the last release before the loader changed, with a two-file schema:

ts
// src/db/schema/posts.ts
import { integer, pgTable, serial, text } from 'drizzle-orm/pg-core';
import { users } from './users.js';
 
export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  authorId: integer('author_id').notNull().references(() => users.id),
});

tsc accepts the project, and drizzle-kit generate fails on the second line of posts.ts:

text
$ npx tsc --noEmit
tsc exit=0
$ npx drizzle-kit generate   # drizzle-kit 0.31.9
No config path provided, using default 'drizzle.config.ts'
Reading config file '/app/drizzle.config.ts'
Error: Cannot find module './users.js'
Require stack:
- /app/src/db/schema/posts.ts
- /app/node_modules/drizzle-kit/bin.cjs
    at Function.<anonymous> (node:internal/modules/cjs/loader:1430:15)
    at Module._resolveFilename (/app/node_modules/drizzle-kit/bin.cjs:16806:40)
  code: 'MODULE_NOT_FOUND',

Two lines tell you which case you are in. The require stack ends in drizzle-kit/bin.cjs, so the import is being resolved by drizzle-kit's CommonJS loader, not by Node's ES module loader. And the file it cannot find is a .js name that exists on disk only as a .ts file.

This was reproduced on 22 October 2026 in the official node:22-bookworm-slim Docker image (Node.js 22.23.3), with drizzle-orm 0.45.4 and TypeScript 7.0.2, in a project with "type": "module" and this tsconfig.json:

json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

Why the .js specifier never reaches users.ts#

Under nodenext, TypeScript models how Node.js will run the compiled output. Node's ES module loader does not guess extensions, so a relative import must name the file that will exist after compilation: ./users.js, not ./users and not ./users.ts. That is why the import looks wrong for a .ts source and is still correct.

The drizzle-kit generate docs explain that the command reads through your Drizzle schema files to build a JSON snapshot, so it has to execute your TypeScript. Up to 0.31.9, the bundled bin.cjs does that with esbuild-register, a CommonJS require hook. Its patch of Node's resolver, visible in the installed 0.31.9 bin.cjs, only handles tsconfig path aliases:

js
Module._resolveFilename = function(request, _parent) {
  const isCoreModule = _module2.builtinModules.includes(request);
  if (!isCoreModule) {
    const found = matchPath(request);
    if (found) {
      const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
      return originalResolveFilename.apply(this, modifiedArguments);
    }
  }
  return originalResolveFilename.apply(this, arguments);
};

./users.js is not an alias, so it falls through to Node's own CommonJS resolver, which looks for src/db/schema/users.js, does not try users.ts in its place, and throws MODULE_NOT_FOUND. The issue author's temporary patch adds exactly that missing step: when a request ends in .js and a .ts file with the same name exists, resolve that instead.

The drizzle-kit 0.31.10 release notes say the CLI migrated away from esbuild-register to the tsx loader, so that it works with both ESM and CJS modules. The notes do not mention issue #3348, and the issue is still open. What the reproduction shows is that the tsx loader resolves ./users.js to users.ts where the old hook did not.

Fix#

1. Upgrade drizzle-kit#

Change nothing but the drizzle-kit version. Pin the 0.31 line rather than moving to the beta or rc tags: those carry the drizzle-kit rewrite that the maintainers' status comment on the issue describes, with a new migration folder structure, and that is a separate upgrade from this fix.

bash
npm install -D drizzle-kit@^0.31.11
npm ls drizzle-kit

On the reproduction project, moving from 0.31.9 to 0.31.10 with no other change produced the migration:

text
$ npx drizzle-kit generate   # drizzle-kit 0.31.10
No config path provided, using default 'drizzle.config.ts'
Reading config file '/app/drizzle.config.ts'
2 tables
posts 3 columns 0 indexes 1 fks
users 2 columns 0 indexes 0 fks

The current release at the time of writing, 0.31.11, gave the same result. The 1.0 beta line depends on jiti rather than tsx, and was not tested here.

2. If you are pinned to 0.31.9 or older, run the CLI under tsx#

A commenter on the issue suggested starting the CLI's bin.cjs through tsx instead of through npx drizzle-kit. drizzle-kit 0.31.9 does not depend on tsx, so add it first:

bash
npm install -D tsx
npx tsx node_modules/drizzle-kit/bin.cjs generate

With drizzle-kit 0.31.9 and tsx 4.23.15, the same schema generated its migration:

text
$ npx tsx node_modules/drizzle-kit/bin.cjs generate   # 0.31.9 under tsx
No config path provided, using default 'drizzle.config.ts'
Reading config file '/app/drizzle.config.ts'
2 tables
posts 3 columns 0 indexes 1 fks
users 2 columns 0 indexes 0 fks

Put it in a script so nobody types the long path:

json
{
  "scripts": {
    "db:generate": "tsx node_modules/drizzle-kit/bin.cjs generate"
  }
}

3. Or point schema at the compiled JavaScript#

Another workaround from the thread: let tsc emit the schema, then give drizzle-kit the emitted .js files, where ./users.js really exists:

ts
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
 
export default defineConfig({
  dialect: 'postgresql',
  schema: './dist/db/schema/*.js',
  out: './drizzle',
});
bash
npx tsc
npx drizzle-kit generate

This works on 0.31.9 too, but it only sees the schema as of the last build. Chain the two commands in one script, or a generate run after editing schema.ts without rebuilding compares against stale code.

Verify the fix#

Run generate and read the output, not only the exit code. On drizzle-kit 0.31.9, the failed run above still returned success to the shell:

text
$ npx drizzle-kit generate > out.txt 2>&1; echo "exit=$?"   # 0.31.9
exit=0

So a CI job on 0.31.9 can stay green while producing no migration. After the fix, the output lists your tables and names the new SQL file, and the drizzle folder contains it. For the two-file schema, the foreign key from posts to users, the import that used to fail, now appears in the SQL:

text
ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

If you added a script that runs generate in CI, also check that the expected .sql file exists, at least until you have confirmed how your drizzle-kit version reports a schema that fails to load.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Dropping the .js extension trades one error for another#

The tempting shortcut is to write import { users } from './users' so the old loader finds the file. Under "module": "nodenext", tsc refuses that import with TS2835 and asks for the .js name, and plain Node.js cannot load the compiled output without it either. The page on ERR_MODULE_NOT_FOUND for a relative import without .js walks through that error and the matching TypeScript setting. The same strict rule also forbids importing a folder; if your schema has import * as schema from './schema' pointing at a directory, see Directory import is not supported.

One commenter moved the whole project to "moduleResolution": "bundler" and adds extensions at build time with a separate tool. That makes drizzle-kit happy at the cost of changing how every file in the project compiles, which is a larger decision than this error.

After generate works: what fails next#

The old loader has a sibling failure on 0.31.9 and older, but only on a Node.js release that cannot require() ES modules: a schema or config that pulls in an ES module package stops with SyntaxError: Cannot use import statement outside a module. On a Node.js release that can, Node loads such a package itself and that error does not appear; the linked page lists which releases those are. If that is what you see instead of this error, the page on drizzle-kit and Cannot use import statement outside a module covers it. Once generate works, drizzle-kit migrate is the next step; if it prints Using 'pg' driver for database querying and then nothing, read drizzle-kit migrate hanging after the pg driver line, which also notes that since 0.31.10 a failed migrate exits with code 1. And if your tables live in a custom pgSchema and db.query then answers relation "user" does not exist, Drizzle db.query dropping pgSchema covers the drizzle-orm versions that lose the schema name.

The .js import in a .ts file is correct TypeScript for Node.js. The failure was drizzle-kit's older loader not following it, so the durable fix is the newer drizzle-kit, and the two workarounds are for projects that cannot move yet.

Further reading: drizzle-kit/api: Dynamic require of "fs" is not supported.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote