How to Set Up Drizzle ORM with Next.js
·PkgPulse Team
TL;DR
Drizzle + Next.js + Neon is the 2026 serverless-ready database stack. Drizzle ORM gives you SQL-like TypeScript queries with full type inference, tiny bundle size (~60KB), and first-class serverless support. Neon (serverless PostgreSQL) connects in milliseconds without connection pooling issues. Setup takes 15 minutes.
Key Takeaways
- Schema = types: Drizzle infers TypeScript types directly from your schema definition
- SQL-like API: if you know SQL, you know Drizzle queries
- Serverless-ready: no connection pool issues (unlike Prisma in serverless)
- Migrations:
drizzle-kit pushfor dev,migratefor production - ~60KB bundle: 10x smaller than Prisma Client
Step 1: Install
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit dotenv
# Or for PlanetScale (MySQL):
npm install drizzle-orm @planetscale/database
npm install -D drizzle-kit
# Or for local PostgreSQL:
npm install drizzle-orm postgres
Step 2: Schema Definition
// src/db/schema.ts
import {
pgTable, // PostgreSQL table
serial,
varchar,
text,
boolean,
timestamp,
integer,
index,
uniqueIndex,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Users table
export const users = pgTable('users', {
id: serial('id').primaryKey(),
clerkId: varchar('clerk_id', { length: 256 }).unique(), // If using Clerk
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 320 }).unique().notNull(),
image: varchar('image', { length: 500 }),
plan: varchar('plan', { length: 20 }).default('free').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
clerkIdIdx: index('users_clerk_id_idx').on(table.clerkId),
}));
// Posts table
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id').references(() => users.id).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
authorIdx: index('posts_author_id_idx').on(table.authorId),
}));
// Relations (for include/joins)
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
// TypeScript types (automatically inferred from schema):
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
Step 3: Database Connection
// src/db/index.ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
export type Database = typeof db;
Step 4: drizzle.config.ts
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle', // Migration files location
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
# Package.json scripts
{
"scripts": {
"db:push": "drizzle-kit push", # Push schema to DB (dev)
"db:migrate": "drizzle-kit migrate", # Generate + apply migrations
"db:generate": "drizzle-kit generate", # Generate migration files only
"db:studio": "drizzle-kit studio" # Open Drizzle Studio GUI
}
}
# Development workflow:
npm run db:push # Sync schema to local DB (no migration files)
# Production workflow:
npm run db:generate # Generate migration SQL
npm run db:migrate # Apply migrations
Step 5: Queries
// src/db/queries.ts — query examples
import { db } from './index';
import { users, posts } from './schema';
import { eq, desc, and, like, count, sql } from 'drizzle-orm';
// ─── SELECT ────────────────────────────────────────────────────────────
// Find one user
const user = await db.select()
.from(users)
.where(eq(users.email, 'alice@example.com'))
.limit(1);
// user: User[] — index [0] to get the row
// Find with specific fields
const emails = await db.select({ id: users.id, email: users.email })
.from(users);
// Exactly typed: { id: number; email: string }[]
// Find with relation (JOIN)
const postsWithAuthors = await db.select({
post: posts,
author: { name: users.name, email: users.email },
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
// Using query API (simpler for relations)
const user = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
where: eq(posts.published, true),
limit: 5,
},
},
});
// ─── INSERT ────────────────────────────────────────────────────────────
const [newUser] = await db.insert(users)
.values({ name: 'Alice', email: 'alice@example.com' })
.returning();
// newUser: User — full row returned
// Batch insert
await db.insert(posts).values([
{ title: 'Post 1', authorId: 1 },
{ title: 'Post 2', authorId: 1 },
]);
// ─── UPDATE ────────────────────────────────────────────────────────────
const [updated] = await db.update(users)
.set({ name: 'Alice Smith', updatedAt: new Date() })
.where(eq(users.id, 1))
.returning();
// ─── DELETE ────────────────────────────────────────────────────────────
await db.delete(posts)
.where(and(eq(posts.authorId, 1), eq(posts.published, false)));
Step 6: Server Actions with Drizzle
// src/app/actions.ts — Next.js Server Actions
'use server';
import { db } from '@/db';
import { posts } from '@/db/schema';
import { auth } from '@/auth';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const [post] = await db.insert(posts)
.values({
title,
content,
authorId: parseInt(session.user.id),
})
.returning();
revalidatePath('/dashboard');
return post;
}
Compare Drizzle and Prisma on PkgPulse.
See the live comparison
View prisma vs. drizzle on PkgPulse →