Home / Articles

Prisma Without Prisma Client: Type-Safe Kysely Queries in Next.js

Learn how to keep Prisma for schema work, generate Kysely types with prisma-kysely, pick a Neon or pg dialect, and query type-safely from Next.js server code.

Publisher: REACTAPP.TOP6 min read1411 words

This article is published in English.

Sources: **Using Prisma for Schema and Kysely for Queries in a Next.js App**

Prisma is excellent for schema design, but Prisma Client is a sizeable dependency to ship into every serverless function when all you want is type-safe SQL. A practical split: Prisma manages the schema during development, a generator turns it into TypeScript types, and Kysely runs the queries in production. This guide sets that up in a Next.js App Router project and compares a Neon HTTP dialect with a classic pg pool.

If you want the conventional route with Prisma Client at runtime instead, see setting up Prisma 7 in a Next.js 16 project.

How the responsibilities divide

Each tool in this setup has one clear job:

  • Prisma owns the database design: the schema file, syncing or migrating the database, and tools such as Prisma Studio for browsing data.
  • Kysely is the runtime query builder. You write SQL-shaped queries in TypeScript, the compiler checks table and column names, and the library itself is small.
  • prisma-kysely is the glue: a Prisma generator that emits Kysely type definitions straight from your Prisma schema.

Prisma stays in your developer workflow, while Kysely is the only database layer in deployed code. The trade-off: you lose Prisma Client's relation-aware API, such as nested include and nested writes, and write joins yourself. If you are still choosing a data layer in general, the comparison of raw SQL, Prisma and Drizzle covers the wider decision.

Installing the packages

The runtime needs Kysely plus a dialect and a driver; this installs the Neon-based stack (pnpm here, but npm or yarn work too):

pnpm add kysely kysely-neon @neondatabase/serverless
pnpm add -D prisma prisma-kysely

Note that prisma and prisma-kysely are dev dependencies: they only manage the schema and generate code.

Defining the schema and generating Kysely types

The schema is ordinary Prisma, except that the generator is prisma-kysely (writing into ./types) instead of the usual client generator. It defines User and Post with a one-to-many relation.

// db/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator kysely {
  provider = "prisma-kysely"
  output   = "./types"
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String  @unique
  posts Post[]
}
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  authorId  Int
  createdAt DateTime @default(now())
  author    User     @relation(fields: [authorId], references: [id])
}

prisma generate now produces Kysely types rather than a client, and prisma db push applies the schema to the database:

pnpm prisma generate
pnpm prisma db push

The output is a file such as db/types/kysely.d.ts with a DB interface describing every table. Three caveats:

  • The schema lives in db/schema.prisma, not Prisma's default prisma/schema.prisma, so point the CLI at it (for example with --schema) as your Prisma version requires.
  • db push syncs the database without recording migration files. For production data, prisma migrate dev and prisma migrate deploy give versioned migrations with the rest of the workflow unchanged.
  • Recent Prisma releases move connection settings into a separate config file; if the url line is rejected, check the current docs.

Package scripts for the everyday loop

A few scripts make the workflow obvious to the team:

{
  "scripts": {
    "db:push": "prisma db push",
    "db:generate": "prisma generate",
    "db:studio": "prisma studio"
  }
}

With these in place, a schema change comes down to two commands:

# To generate the types
pnpm db:generate
# To push to the remote DB
pnpm db:push

The database and the Kysely types then stay aligned, so the compiler flags any query touching a renamed or removed column.

Picking a Kysely dialect

Kysely does not talk to the database itself. A dialect sits between the query builder and the driver and knows how to compile and send queries for one database and connection method. Official dialects cover PostgreSQL, MySQL and SQLite; community ones cover services such as Neon, PlanetScale and Turso. For Postgres, two options cover most cases.

Neon over HTTP for serverless deployments

On Vercel or a similar platform, Neon's HTTP driver fits well: each query is a stateless request, so there is no persistent connection to maintain between short invocations. The setup wraps neon() in NeonDialect and types Kysely with the generated DB:

// db/index.ts
import { Kysely } from 'kysely'
import { NeonDialect } from 'kysely-neon'
import { neon } from '@neondatabase/serverless'
import type { DB } from './types/kysely'

const db = new Kysely<DB>({
  dialect: new NeonDialect({
    neon: neon(process.env.DATABASE_URL!),
  }),
})

export default db

The ! only silences TypeScript; it validates nothing. The bigger limitation is that kysely-neon over HTTP cannot run transactions or session-based queries, so it suits simple CRUD and read-heavy pages rather than multi-statement work. With Neon's WebSocket driver you do not need kysely-neon at all: pass Neon's Pool to Kysely's built-in PostgresDialect. The kysely-neon API has also changed between releases, so compare with its current README.

node-postgres for long-running servers

For a long-lived Node.js server, the official pg driver is the most complete choice, with transactions, pooling and prepared statements. It plugs into Kysely's own PostgresDialect:

// db/index.ts
import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'
import type { DB } from './types/kysely'

const db = new Kysely<DB>({
  dialect: new PostgresDialect({
    pool: new Pool({
      connectionString: process.env.DATABASE_URL,
    }),
  }),
})

export default db

Choosing between them

The difference is the connection model. kysely-neon is stateless HTTP: light and serverless-friendly, but no transactions. pg keeps pooled TCP connections open, with the full feature set, which suits servers that stay up. Use kysely-neon on serverless platforms such as Vercel or Netlify, and pg or the Neon WebSocket pool once you need transactions. Beware a pg pool inside serverless functions: each instance opens its own connections and can exhaust the database limit without an external pooler.

Querying from a Server Component

Any server-side code can import db. This Server Component lists users, selecting three columns ordered by id:

// app/users/page.tsx
import db from '@/db'

export default async function UsersPage() {
  const users = await db
    .selectFrom('user')
    .select(['id', 'name', 'email'])
    .orderBy('id')
    .execute()

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(u => (
          <li key={u.id}>{u.name} ({u.email})</li>
        ))}
      </ul>
    </div>
  )
}

Since DB comes from the Prisma schema, a misspelled table or column fails type-checking, and users is inferred with exactly the selected fields.

One catch: Prisma names tables after models by default, so this schema creates a table User, and the generated types use that name. selectFrom('user') only type-checks if you add @@map("user") to the model; otherwise query 'User'. Also keep db out of Client Components, as it reads a server-only secret.

The day-to-day workflow

Once everything is wired, the loop looks like this:

  1. Change models in db/schema.prisma.
  2. Regenerate types and apply the change with the two scripts.
  3. Use db from Server Components, route handlers or Server Actions.
  4. Validate input with Zod or similar before inserting; Kysely types check query shape, not form values.

The resulting layout keeps everything database-related under db/:

📦 my-next-app
├── 📁 app
│   └── 📁 users
│       └── page.tsx
├── 📁 db
│   ├── schema.prisma
│   ├── index.ts
│   └── 📁 types
│       └── kysely.d.ts
├── 📁 node_modules
├── .env
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json

Wrapping up

This split keeps Prisma's schema workflow and Studio without shipping Prisma Client, and gives you a small SQL-shaped query layer whose types track the schema.

  • Keep prisma and prisma-kysely as dev dependencies; only Kysely, a dialect and a driver ship to production.
  • Use the Neon HTTP dialect for simple serverless workloads, and switch to a pooled PostgresDialect when you need transactions.
  • Watch table naming: generated types follow the database names, which default to the Prisma model names.
  • Move from db push to proper migrations before the data in the database starts to matter.