Skip to main content

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 push for dev, migrate for 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.