Home JavaScript Next.js with Drizzle ORM: Database Integration Guide
Intermediate 3 min · July 12, 2026

Next.js with Drizzle ORM: Database Integration Guide

Complete guide to using Drizzle ORM with Next.js App Router.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 25 min
  • Node.js 18+, Next.js 14+, TypeScript 5+, PostgreSQL (or MySQL/SQLite), basic SQL knowledge, familiarity with Next.js App Router
Quick Answer
  • Drizzle ORM is a lightweight, type-safe SQL ORM with zero runtime dependencies
  • Define schemas in TypeScript and auto-generate SQL migrations
  • Use Drizzle directly in Server Components — no API route needed
  • drizzle-orm + drizzle-kit for schema and migrations
  • Better tree-shaking than Prisma — smaller bundle for client components
  • Supports PostgreSQL, MySQL, SQLite, and Turso
✦ Definition~90s read
What is Next.js with Drizzle ORM?

Next.js with Drizzle ORM is a modern stack for building full-stack applications with type-safe database access. Drizzle ORM provides a lightweight, SQL-like query builder that integrates seamlessly with Next.js server components and API routes, eliminating runtime overhead and enabling compile-time query validation.

Drizzle ORM is like having a TypeScript translator for your database.

Use this stack when you need a production-ready, type-safe database layer without the complexity of traditional ORMs.

Plain-English First

Drizzle ORM is like having a TypeScript translator for your database. You write TypeScript objects and functions, Drizzle converts them to SQL queries with perfect type safety — no raw SQL strings, no runtime surprises, and your editor autocompletes your database schema.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Drizzle ORM has emerged as the leading lightweight alternative to Prisma for Next.js applications. It offers type-safe SQL with zero runtime dependencies, smaller bundle sizes, and a more direct mapping to SQL semantics. For Next.js App Router applications, Drizzle's ability to run directly in Server Components without an API layer makes it particularly attractive.

This guide covers the complete integration: schema setup, migrations, data fetching patterns in Server Components, transactions, and production deployment with PostgreSQL.

Setting Up the Project

Start by creating a Next.js project with TypeScript and installing Drizzle ORM along with a database driver. For PostgreSQL, use drizzle-orm and pg. Initialize Drizzle with drizzle-kit for schema management. Configure your database connection string in a .env file. This setup ensures type-safe queries from the start.

terminalBASH
1
2
3
4
5
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app
npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg
# Create .env.local with DATABASE_URL=postgres://user:pass@localhost:5432/mydb
Output
Successfully created Next.js app with Drizzle dependencies.
Database Driver Choice
For PostgreSQL, use pg. For MySQL, use mysql2. Drizzle supports multiple drivers; pick the one matching your database.
Production Insight
Always store database credentials in environment variables and never commit .env.local to version control. Use a secrets manager in production.
Key Takeaway
Drizzle ORM setup is minimal: install two packages and configure a connection string.
nextjs-drizzle-orm-guide THECODEFORGE.IO Next.js with Drizzle ORM Layered Architecture Component stack from database to UI Database Layer PostgreSQL | MySQL | SQLite ORM Layer Drizzle Client | Schema Definitions | Migration Engine API Layer Next.js API Routes | Server Actions | Route Handlers Server Components React Server Components | Data Fetching | Streaming Client Components Interactive UI | Form Submissions | Optimistic Updates THECODEFORGE.IO
thecodeforge.io
Nextjs Drizzle Orm Guide

Defining the Schema

Define your database schema using Drizzle's schema definition syntax. Create a schema.ts file that exports tables with columns, types, and relations. Drizzle uses a code-first approach: you write TypeScript objects that map to SQL tables. This schema is the single source of truth for your database structure.

db/schema.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});
Output
Schema defined with users and posts tables.
Try it live
Use `serial` for Auto-Increment
serial creates an auto-incrementing integer column. For UUIDs, use uuid type and generate IDs in your app.
Production Insight
Always add notNull constraints and defaults where appropriate to avoid null pointer issues in queries.
Key Takeaway
Schema is defined in TypeScript, ensuring type safety and easy migrations.

Running Migrations

Use drizzle-kit to generate and run migrations. First, configure drizzle.config.ts with your schema path and database connection. Then run npx drizzle-kit generate to create SQL migration files, and npx drizzle-kit migrate to apply them. This keeps your database schema in sync with your code.

drizzle.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
import type { Config } from 'drizzle-kit';

export default {
  schema: './db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
} satisfies Config;
Output
Configuration file for Drizzle Kit.
Try it live
Migration Safety
Always review generated SQL before running migrations in production. Use drizzle-kit push for development only.
Production Insight
In CI/CD, run migrations as a separate step before deploying new code to avoid schema mismatches.
Key Takeaway
Migrations are SQL files you can version control and review.
Drizzle ORM vs Prisma in Next.js Performance, control, and developer experience trade-offs Drizzle ORM Prisma Bundle Size Minimal (tree-shakable) Larger (includes engine) Query Control Full SQL-like syntax Abstracted with Prisma Client Migration Speed Fast (drizzle-kit) Slower (prisma migrate) Type Safety Inferred from schema Generated from schema Learning Curve Steeper (SQL knowledge) Gentler (declarative) THECODEFORGE.IO
thecodeforge.io
Nextjs Drizzle Orm Guide

Connecting to the Database

Create a database client instance using Drizzle's drizzle() function. Pass the connection pool from your driver (e.g., pg). In Next.js, it's best to create a singleton client to avoid multiple connections during hot reloads. Export the db object for use in API routes and server components.

db/index.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool, { schema });
Output
Database client ready for queries.
Try it live
Singleton Pattern
In development, Next.js hot reloads can create many connections. Use a global variable to reuse the pool.
Production Insight
Set connection pool limits (e.g., max: 20) to avoid exhausting database connections under load.
Key Takeaway
A single db instance provides type-safe access to all tables.

Querying Data in Server Components

Next.js server components can directly query the database using Drizzle's query builder. This eliminates the need for API routes for data fetching. Use db.select() to retrieve data, and pass results as props to client components. Drizzle's type inference ensures you get full TypeScript support.

app/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
import { db } from '@/db';
import { posts } from '@/db/schema';

export default async function HomePage() {
  const allPosts = await db.select().from(posts);
  return (
    <ul>
      {allPosts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
Output
Renders a list of post titles.
Try it live
Server Components Only
Database queries in server components are safe and efficient. Never query directly in client components.
Production Insight
Use unstable_noStore or export const dynamic = 'force-dynamic' to prevent caching of dynamic data.
Key Takeaway
Server components fetch data at request time, reducing client-side load.

Using Drizzle Queries with Filters and Joins

Drizzle's query builder supports complex queries with filters, joins, and aggregations. Use where for conditions, innerJoin for relations, and orderBy for sorting. The API is SQL-like but fully typed, preventing runtime errors from typos or mismatched types.

app/posts/[id]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { db } from '@/db';
import { posts, users } from '@/db/schema';
import { eq } from 'drizzle-orm';

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await db
    .select()
    .from(posts)
    .where(eq(posts.id, parseInt(params.id)))
    .innerJoin(users, eq(posts.authorId, users.id))
    .limit(1);
  
  if (!post.length) return <div>Not found</div>;
  return <div>{post[0].posts.title} by {post[0].users.name}</div>;
}
Output
Displays post title and author name.
Try it live
Use `limit(1)` for Single Results
Always limit queries when expecting a single row to avoid unnecessary data transfer.
Production Insight
Add indexes on columns used in where and join clauses to maintain performance at scale.
Key Takeaway
Drizzle's query builder is as expressive as raw SQL but type-safe.

Inserting and Updating Data via API Routes

For mutations, create Next.js API routes that use Drizzle's insert and update methods. Validate input with Zod or similar, then execute the query. Return appropriate HTTP status codes. This pattern keeps your data layer secure and testable.

app/api/posts/route.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { db } from '@/db';
import { posts } from '@/db/schema';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { title, content, authorId } = body;
  
  const newPost = await db.insert(posts).values({
    title,
    content,
    authorId,
  }).returning();
  
  return NextResponse.json(newPost[0], { status: 201 });
}
Output
Creates a new post and returns it.
Try it live
Input Validation
Never trust user input. Use Zod to validate request bodies before inserting into the database.
Production Insight
Use returning() to get the inserted row, avoiding a separate select query.
Key Takeaway
API routes handle mutations with Drizzle's insert and update.

Handling Relations and Transactions

Drizzle supports relational queries and transactions. Use db.query with findMany and with clauses to eagerly load relations. For atomic operations, wrap multiple queries in a transaction using db.transaction(). This ensures data consistency.

app/api/users/[id]/route.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { eq } from 'drizzle-orm';

export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
  const userWithPosts = await db.query.users.findFirst({
    where: eq(users.id, parseInt(params.id)),
    with: {
      posts: true,
    },
  });
  return NextResponse.json(userWithPosts);
}
Output
Returns user with their posts.
Try it live
Relational Queries
Drizzle's relational API is intuitive but can generate N+1 queries if not careful. Use with to batch load.
Production Insight
Always use transactions for operations that modify multiple tables to prevent partial updates.
Key Takeaway
Transactions and relations are first-class citizens in Drizzle.

Error Handling and Logging

Wrap database operations in try-catch blocks and return meaningful error responses. Log errors to a monitoring service (e.g., Sentry) but avoid exposing internal details to the client. Drizzle throws typed errors that you can catch and handle gracefully.

app/api/posts/route.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { db } from '@/db';
import { posts } from '@/db/schema';
import { NextRequest, NextResponse } from 'next/server';

export async function GET() {
  try {
    const allPosts = await db.select().from(posts);
    return NextResponse.json(allPosts);
  } catch (error) {
    console.error('Database error:', error);
    return NextResponse.json(
      { error: 'Failed to fetch posts' },
      { status: 500 }
    );
  }
}
Output
Returns posts or error response.
Try it live
Don't Leak Errors
Never send raw database errors to the client. They may contain sensitive information.
Production Insight
Use a structured logging library (e.g., pino) and aggregate logs in a central system like Datadog.
Key Takeaway
Always handle errors gracefully and log for debugging.

Testing Database Queries

Write integration tests for your database layer using a test database or in-memory SQLite. Drizzle's driver-agnostic design allows swapping to better-sqlite3 for testing. Use vitest or jest to run queries and assert results.

tests/db.test.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { describe, it, expect, beforeAll } from 'vitest';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from '../db/schema';

const sqlite = new Database(':memory:');
const db = drizzle(sqlite, { schema });

beforeAll(async () => {
  // Run migrations or create tables
  db.run(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`);
});

describe('Users', () => {
  it('should insert and retrieve a user', async () => {
    await db.insert(schema.users).values({ name: 'Test', email: 'test@test.com' });
    const users = await db.select().from(schema.users);
    expect(users.length).toBe(1);
  });
});
Output
Test passes if user is inserted.
Try it live
Use In-Memory DB for Speed
In-memory SQLite is fast and isolated, perfect for unit tests.
Production Insight
Run integration tests in CI with a real PostgreSQL instance to catch driver-specific bugs.
Key Takeaway
Test your database layer with a lightweight driver to catch issues early.

Deploying to Production

When deploying, ensure your database connection string is set as an environment variable. Use connection pooling and set pool limits. For serverless environments (Vercel), use a serverless-friendly driver like @neondatabase/serverless with Drizzle. Avoid cold starts by keeping connections alive.

db/index.tsTYPESCRIPT
1
2
3
4
5
6
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool } from '@neondatabase/serverless';
import * as schema from './schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
Output
Serverless-optimized database client.
Try it live
Serverless Connection Limits
Serverless functions have connection limits. Use a connection pooler like PgBouncer or Neon's built-in pooling.
Production Insight
Monitor connection pool usage and set alerts for exhaustion. Use connectionTimeoutMillis to avoid hanging queries.
Key Takeaway
Choose the right driver for your deployment target.
⚙ Quick Reference
11 commands from this guide
FileCommand / CodePurpose
terminalnpx create-next-app@latest my-app --typescript --tailwind --eslintSetting Up the Project
dbschema.tsexport const users = pgTable('users', {Defining the Schema
drizzle.config.tsexport default {Running Migrations
dbindex.tsconst pool = new Pool({Connecting to the Database
apppage.tsxexport default async function HomePage() {Querying Data in Server Components
appposts[id]page.tsxexport default async function PostPage({ params }: { params: { id: string } }) {Using Drizzle Queries with Filters and Joins
appapipostsroute.tsexport async function POST(request: NextRequest) {Inserting and Updating Data via API Routes
appapiusers[id]route.tsexport async function GET(request: NextRequest, { params }: { params: { id: stri...Handling Relations and Transactions
appapipostsroute.tsexport async function GET() {Error Handling and Logging
testsdb.test.tsconst sqlite = new Database(':memory:');Testing Database Queries
dbindex.tsconst pool = new Pool({ connectionString: process.env.DATABASE_URL });Deploying to Production

Key takeaways

1
Type-Safe Queries
Drizzle ORM provides compile-time validation of SQL queries, eliminating runtime errors from typos or schema mismatches.
2
Minimal Overhead
Unlike heavy ORMs, Drizzle is a thin wrapper around SQL, resulting in near-native performance and small bundle size.
3
Seamless Next.js Integration
Use Drizzle directly in server components and API routes without additional layers, leveraging Next.js data fetching patterns.
4
Production-Ready Migrations
Drizzle Kit generates SQL migration files that are easy to review and version control, ensuring safe schema changes.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Drizzle ORM and why use it with Next.js?
02
How do I handle migrations in production?
03
Can I use Drizzle with serverless databases like Neon or PlanetScale?
04
How do I avoid N+1 queries with Drizzle?
05
Is Drizzle ORM production-ready?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

3 min read · try the examples if you haven't

Previous
Next.js Project Structure: Folder Organization Best Practices
51 / 56 · Next.js
Next
Next.js with Shadcn/ui: Component Library Quickstart