Next.js with Drizzle ORM: Database Integration Guide
Complete guide to using Drizzle ORM with Next.js App Router.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- ✓Node.js 18+, Next.js 14+, TypeScript 5+, PostgreSQL (or MySQL/SQLite), basic SQL knowledge, familiarity with Next.js App Router
- 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
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
pg. For MySQL, use mysql2. Drizzle supports multiple drivers; pick the one matching your database..env.local to version control. Use a secrets manager in production.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.
serial creates an auto-incrementing integer column. For UUIDs, use uuid type and generate IDs in your app.notNull constraints and defaults where appropriate to avoid null pointer issues in queries.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-kit push for development only.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.
max: 20) to avoid exhausting database connections under load.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.
unstable_noStore or export const dynamic = 'force-dynamic' to prevent caching of dynamic data.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.
where and join clauses to maintain performance at scale.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.
returning() to get the inserted row, avoiding a separate select query.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 . This ensures data consistency.db.transaction()
with to batch load.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.
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.
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.
connectionTimeoutMillis to avoid hanging queries.| File | Command / Code | Purpose |
|---|---|---|
| terminal | npx create-next-app@latest my-app --typescript --tailwind --eslint | Setting Up the Project |
| db | export const users = pgTable('users', { | Defining the Schema |
| drizzle.config.ts | export default { | Running Migrations |
| db | const pool = new Pool({ | Connecting to the Database |
| app | export default async function HomePage() { | Querying Data in Server Components |
| app | export default async function PostPage({ params }: { params: { id: string } }) { | Using Drizzle Queries with Filters and Joins |
| app | export async function POST(request: NextRequest) { | Inserting and Updating Data via API Routes |
| app | export async function GET(request: NextRequest, { params }: { params: { id: stri... | Handling Relations and Transactions |
| app | export async function GET() { | Error Handling and Logging |
| tests | const sqlite = new Database(':memory:'); | Testing Database Queries |
| db | const pool = new Pool({ connectionString: process.env.DATABASE_URL }); | Deploying to Production |
Key takeaways
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's Next.js. Mark it forged?
3 min read · try the examples if you haven't