prismasqlitenextjsdatabase

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.

May 24, 202610 min readBy Himanshu Jain

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 with better-sqlite3 directly. 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
  • tsx for 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 in tsconfig.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.json or a long --compiler-options flag to work at all. tsx was built specifically to handle TypeScript in modern Node.js environments. It wraps esbuild under the hood, handles ESM and CJS transparently, and requires zero configuration. Install it once and your seed command becomes just tsx 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:

  1. The @/ path alias won't resolve in tsx without extra tsconfig-paths configuration.
  2. The globalThis caching 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

ErrorRoot CauseFix
Cannot find module generated/prisma/clientClient not generated; moduleResolution: "bundler" breaks ts-nodeRun prisma generate; switch to tsx
exports is not defined in ES module scopets-node can't handle ESM/CJS boundary in generated clientSwitch to tsx
accelerateUrl required or invalidScaffolded boilerplate for Prisma Accelerate — not needed for SQLiteUse adapter option with PrismaBetterSQLite3
PrismaLibSQL not exportedWrong import name from outdated docs/examplesUse 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.