Prisma + SQLite + better-sqlite3 in Next.js: Every Error I Hit and How I Fixed Them
Setting up Prisma with SQLite in a Next.js project sounds like a twenty-minute task. Add the packages, write a schema, generate the client, seed the database — done. Except when you layer in driver adapters, a monorepo tsconfig tuned for Next.js internals, and a `prisma.config.ts` that was auto-generated with Accelerate boilerplate, the whole thing turns into a debugging session that touches Node.js module systems, TypeScript compiler options, and Prisma's own generated output format.
This post documents every error I hit — in sequence — while setting this up, what was actually causing it, and how to fix it properly rather than papering over it.
The Stack
Before getting into errors, here's what the project is using:
- Next.js 14 (App Router)
- Prisma 7.x with
@prisma/client @prisma/adapter-better-sqlite3— a driver adapter that replaces Prisma's native query engine withbetter-sqlite3directly. This is the modern way to use SQLite with Prisma and avoids shipping a binary engine.- SQLite with
DATABASE_URL="file:./dev.db"— local development database, no cloud services involved tsxfor running TypeScript scripts like seeds without a build step The project is a Next.js monorepo where the tsconfig is configured for the bundler pipeline, which turns out to be the root cause of most of the pain below.
A Note on ts-node vs tsx
Before getting into individual errors: the recommendation here is to skip ts-node entirely and use tsx for any script that runs outside Next.js (seeds, migrations, one-off scripts, etc.).
Here's why ts-node is painful in this context:
- It does not understand
"moduleResolution": "bundler", which is what Next.js sets intsconfig.json. You have to override it every time. - It struggles with the ESM/CJS boundary when your generated Prisma client is ESM-first.
- It requires either a separate
tsconfig.seed.jsonor a long--compiler-optionsflag to work at all.tsxwas built specifically to handle TypeScript in modern Node.js environments. It wrapsesbuildunder the hood, handles ESM and CJS transparently, and requires zero configuration. Install it once and your seed command becomes justtsx prisma/seed.ts.
npm install --save-dev tsx
With that established, here is every error that came up while setting this up.
Error 1: Cannot find module '.../generated/prisma/client'
The full error
Error: Cannot find module 'C:\Users\...\project\generated\prisma\client'
at finalizeResolution (node:internal/modules/esm/resolve:274:11)
...
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///C:/Users/.../project/generated/prisma/client'
This came from running the seed command that Prisma's config scaffolded:
ts-node --compiler-options {"module":"CommonJS"} -r tsconfig-paths/register prisma/seed.ts
What's actually happening
There are two separate problems hitting at the same time, which makes this error misleading.
Problem one: the client doesn't exist yet. The generated/prisma/client directory is produced by prisma generate. If you haven't run that command, or if the output path in your schema.prisma generator block is wrong, the directory simply isn't there. The import in seed.ts points at nothing.
Problem two: moduleResolution: "bundler" breaks ts-node. The project's tsconfig.json has this because Next.js requires it for its own build pipeline:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
}
}
"bundler" is a TypeScript 5.0+ setting that tells the compiler to resolve modules the way a bundler like Webpack or Turbopack would — not the way Node.js would. ts-node runs in Node.js, so it cannot understand this resolution strategy. The result is it fails to find any module that uses non-Node.js path conventions, including the Prisma generated client.
The fix
Step 1: Make sure your schema.prisma generator block has the correct output path and driverAdapters preview feature enabled:
generator client {
provider = "prisma-client-js"
output = "../generated/prisma/client"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
Step 2: Generate the client:
npx prisma generate
Step 3: Drop ts-node entirely. Install tsx and update the seed command in prisma.config.ts:
seed: 'tsx prisma/seed.ts',
tsx resolves modules the way Node.js does natively, so there's no conflict with tsconfig.json's moduleResolution setting. It just works.
Error 2: exports is not defined in ES module scope
The full error
exports is not defined in ES module scope
at file:///C:/Users/.../generated/prisma/client.ts:48:23
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:450:37)
...
This came up after attempting to fix Error 1 by switching moduleResolution to "node" and using a tsconfig.seed.json, rather than switching to tsx.
What's actually happening
This is the ESM/CJS boundary problem. When Prisma generates its client with previewFeatures = ["driverAdapters"], the output is ESM-first. Node.js treats .ts files under an ESM module context differently than CJS, and ts-node — even when told to compile as CommonJS — can end up in a state where it's trying to use CJS semantics (exports, require) on a file that Node.js is loading as an ES module. The exports is not defined error is the runtime crash that results.
This is exactly the scenario tsx was built to handle. It uses esbuild to transpile TypeScript on the fly and handles the ESM/CJS interop at the transpilation layer rather than relying on Node.js to sort it out at runtime.
The fix
Switch to tsx. This is the same fix as Error 1 — the earlier errors were all symptoms of using ts-node in an environment it isn't suited for.
npm install --save-dev tsx
Update prisma.config.ts:
migrations: {
path: "prisma/migrations",
seed: 'tsx prisma/seed.ts',
},
Run the seed directly:
npx tsx prisma/seed.ts
If you hit this error, stop trying to fix ts-node's configuration. The tool is the wrong choice for this environment.
Error 3: accelerateUrl is required / Invalid URL
The full error
Two variants of this appeared at different points:
Error validating `accelerateUrl`, the URL cannot be parsed, reason: Invalid URL
code: 'P6001'
And as a TypeScript type error:
Argument of type '{ log: ("query" | "warn" | "error")[]; }' is not assignable to
parameter of type 'Subset<PrismaClientOptions, PrismaClientOptions>'.
Property 'accelerateUrl' is missing in type '{ log: ... }'
but required in type '...'
What's actually happening
When Prisma scaffolds a prisma.config.ts, it sometimes generates a PrismaClient instantiation with accelerateUrl:
const prisma = new PrismaClient({
accelerateUrl: "test",
});
This is boilerplate for Prisma Accelerate, which is a managed connection pooling and query caching service. It's designed for edge runtimes and serverless environments where you can't maintain a persistent database connection. It requires a special prisma+postgres:// connection string and a paid Prisma platform account.
None of that applies here. The database is a local SQLite file at file:./dev.db. There is no Accelerate account, no edge runtime, no connection pooling needed. The accelerateUrl: "test" was dead scaffolding that caused a runtime crash when Prisma tried to parse it as a real URL.
The TypeScript error is a related issue: when previewFeatures = ["driverAdapters"] is set in schema.prisma, the generated PrismaClient type changes its constructor signature. Without a driver adapter or accelerateUrl, TypeScript rejects the options object as invalid.
What Prisma Accelerate is (and when you'd actually want it)
Accelerate makes sense when:
- You're deploying to a serverless platform like Vercel or Cloudflare Workers where each function invocation spins up a fresh process
- You need a connection pool in front of a PostgreSQL/MySQL database to avoid exhausting connections
- You want query caching at the edge without managing Redis yourself
For local SQLite development with
better-sqlite3, Accelerate is irrelevant. The adapter handles the database connection directly.
The fix
Remove accelerateUrl entirely. Use the driver adapter pattern instead:
import { PrismaClient } from "../generated/prisma/client";
import Database from "better-sqlite3";
import { PrismaBetterSQLite3 } from "@prisma/adapter-better-sqlite3";
const sqlite = new Database("./dev.db");
const adapter = new PrismaBetterSQLite3(sqlite);
const prisma = new PrismaClient({ adapter });
The adapter option is what satisfies the constructor's type requirements when driverAdapters is enabled. No accelerateUrl needed.
Error 4: PrismaLibSQL has no exported member
The full error
Module '"@prisma/adapter-better-sqlite3"' has no exported member 'PrismaLibSQL'
What's actually happening
A lot of blog posts, official docs examples, and AI-generated code use PrismaLibSQL as the import name for the better-sqlite3 adapter. This name comes from an earlier version of the adapter API or from LibSQL (the Turso fork of SQLite), and it is wrong for @prisma/adapter-better-sqlite3.
The @prisma/adapter-better-sqlite3 package uses its own named export that matches the package it wraps.
The fix
Use the correct export name:
import { PrismaBetterSQLite3 } from "@prisma/adapter-better-sqlite3";
This is a one-line fix, but it's easy to miss if you're copying from examples that reference the wrong name.
Final Architecture: Shared Client Factory
Once all the errors are resolved, the question is how to structure the Prisma client so it can be used in both the Next.js application and the seed script without duplicating the adapter setup.
The problem with naively sharing code
The Next.js singleton pattern looks like this:
import { PrismaClient } from "@/generated/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({ log: ["query", "error", "warn"] });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
The singleton exists because Next.js hot-reloads modules in development. Without caching the PrismaClient instance on globalThis, every file save would create a new database connection and you'd quickly exhaust the connection limit. In production the module is loaded once, so it's a no-op.
You can't directly import this from a seed script because:
- The
@/path alias won't resolve intsxwithout extratsconfig-pathsconfiguration. - The
globalThiscaching logic is completely pointless in a seed script — the process exits after running, so there's no hot reload to guard against.
The solution: a factory function
Extract the adapter setup into a shared factory that both consumers can call:
// lib/prisma/client.ts
import { PrismaClient } from "@/generated/prisma/client";
import Database from "better-sqlite3";
import { PrismaBetterSQLite3 } from "@prisma/adapter-better-sqlite3";
export function createPrismaClient() {
const sqlite = new Database("./dev.db");
const adapter = new PrismaBetterSQLite3(sqlite);
return new PrismaClient({ adapter });
}
The Next.js singleton uses it via the path alias:
// lib/prisma/index.ts
import { createPrismaClient } from "@/lib/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | undefined;
};
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
The seed script uses a relative import to sidestep the alias resolution issue:
// prisma/seed.ts
import { createPrismaClient } from "../lib/prisma/client";
const prisma = createPrismaClient();
async function main() {
console.log("Seeding...");
// seed logic here
}
main()
.catch(console.error)
.finally(() => process.exit());
This gives you a single place where the adapter is configured. If you ever change the database path, the adapter version, or add logging, you change it in lib/prisma/client.ts and both consumers get the update.
Summary
| Error | Root Cause | Fix |
|---|---|---|
Cannot find module generated/prisma/client | Client not generated; moduleResolution: "bundler" breaks ts-node | Run prisma generate; switch to tsx |
exports is not defined in ES module scope | ts-node can't handle ESM/CJS boundary in generated client | Switch to tsx |
accelerateUrl required or invalid | Scaffolded boilerplate for Prisma Accelerate — not needed for SQLite | Use adapter option with PrismaBetterSQLite3 |
PrismaLibSQL not exported | Wrong import name from outdated docs/examples | Use PrismaBetterSQLite3 |
The common thread through errors 1 and 2 is ts-node. It is the wrong tool for this environment. tsx handles everything ts-node struggles with here — ESM/CJS interop, modern module resolution, TypeScript paths — with zero configuration. Install it once and stop fighting the toolchain.
The common thread through errors 3 and 4 is that AI-generated and auto-scaffolded code often contains stale patterns. accelerateUrl is real but irrelevant here. PrismaLibSQL is a real name but for the wrong package. Always verify import names and constructor options against the actual installed package version, not documentation that may be a major version behind.