DEV Community

Cover image for Using Prisma for Schema and Kysely for Queries in a Next.js App
Golam Rabbani
Golam Rabbani

Posted on

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

When I first started using Prisma, I loved how easy it made database work - migrations, schema changes, Prisma Studio, all just worked.

But once I started deploying more apps on Vercel and serverless environments, I noticed something: Prisma Client felt a bit heavy.

Every time I deployed, my bundle size grew. And I didn't really need the full ORM - I just wanted a way to manage my schema and write type-safe SQL queries.

That's when I found a good balance:
👉 Use Prisma only for schema management, and Kysely for queries.

This setup has been super clean for me in my Next.js projects - and in this post, I'll show you exactly how to do it.

Why Prisma + Kysely Combo

I like to think of it like this:

  • Prisma is my database architect. It manages the schema, migrations, and gives me tools like prisma studio.
  • Kysely is my query builder. It lets me write SQL in TypeScript with full type safety and a smaller runtime size.
  • The prisma-kysely generator connects both worlds - it generates Kysely types directly from your Prisma schema.

So, Prisma stays for the dev experience.

Kysely takes over at runtime.

No Prisma Client in production - just lean, type-safe SQL.


Step 1: Install Dependencies

Let's start by adding the right packages.

I'm using pnpm here, but you can also use npm or yarn.
I just prefer pnpm because it's faster and saves disk space.

pnpm add kysely kysely-neon @neondatabase/serverless
pnpm add -D prisma prisma-kysely
Enter fullscreen mode Exit fullscreen mode

Notice that prisma is a dev dependency, because we'll use it only for schema and code generation - not at runtime.


Step 2: Write Your Prisma Schema

Here's a small and simple schema with User and Post models.

// 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])
}
Enter fullscreen mode Exit fullscreen mode

Now run the following commands to sync the schema and generate Kysely types:

pnpm prisma generate
pnpm prisma db push
Enter fullscreen mode Exit fullscreen mode

This will create a file like db/types/kysely.d.ts which includes all your types.


Add helpful scripts to your package.json

It's a good idea to add a few Prisma scripts for convenience:

{
  "scripts": {
    "db:push": "prisma db push",
    "db:generate": "prisma generate",
    "db:studio": "prisma studio"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now whenever you change something in the Prisma schema, just run:

# To generate the types
pnpm db:generate
# To push to the remote DB
pnpm db:push
Enter fullscreen mode Exit fullscreen mode

This will update both the database and the Kysely types automatically


Step 3: Choose a Kysely Dialect

Kysely doesn't connect to your database directly - it uses a dialect.

You can think of it as the "bridge" that knows how to talk to your database.

Some dialects are official (like PostgreSQL, MySQL, SQLite), and others are community-built (like Neon, PlanetScale, Turso, etc.).

Let's see two popular options 👇


🟢 Option 1: Neon (Serverless) Setup

If you're deploying to Vercel or another serverless platform, Neon is great.

It uses an HTTP-based connection, so you don't need to maintain a persistent database connection.

// 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
Enter fullscreen mode Exit fullscreen mode

However, keep this in mind:

kysely-neon runs over HTTP, which means no transaction queries or session-based operations.
It's perfect for simple CRUD or read-heavy apps, but not for complex transactions.

Also note:

If you use WebSockets with Neon, you don't need kysely-neon.
You can just use Neon's Pool with Kysely's built-in Postgres dialect.


🟣 Option 2: Postgres (pg) Setup

If you're running a traditional Node.js or edge-compatible backend, the official pg driver is still the best.

It supports full transactions, connection pooling, and prepared statements.

// 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
Enter fullscreen mode Exit fullscreen mode
Dialect Connection Type Supports Transactions Best For
kysely-neon HTTP (Serverless) ❌ No Vercel, Netlify, Cloudflare
pg TCP (Persistent) ✅ Yes Node.js servers or edge backends

So, in short:

Use kysely-neon for serverless setups, and pg when you need full database features like transactions.


Step 4: Use Kysely in a Next.js Server Component

Let's try a small example - fetching all users in a server component.

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

This query is completely type-safe.

If you type the wrong table or column name, TypeScript will catch it immediately - because Kysely's types come directly from your Prisma schema.


Step 5: Typical Workflow Recap

  1. Update models in db/schema.prisma.
  2. Run pnpm db:generate and pnpm db:push to generate the types and sync changes.
  3. Use the db instance in your Next.js server components, API routes, or actions.
  4. Validate inputs with Zod or any schema library before inserting.

Example Folder Structure

Here's how the full setup looks in a clean folder tree:

📦 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
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

This setup has worked really well for me in my Next.js apps.

I still get all of Prisma's nice tools - schema, migrations, Prisma Studio - but without pulling Prisma Client into production.

Kysely makes my queries simple, type-safe, and fast.

And Prisma keeps my database organized.

If you're using Postgres and want to keep your app lean, type-safe, and serverless-friendly, try this approach once.

It's clean, easy to maintain, and feels great to work with.


✍️ Quick summary

  • Prisma = schema & migrations
  • Kysely = type-safe SQL
  • Together = clean, fast, serverless-ready setup

Top comments (0)