Connect Your Database
Wire up Neon Postgres to your Next.js app with Drizzle ORM. Type-safe queries, migrations, and real data in 20 minutes.
The Stack
Neon
Serverless Postgres. Free tier, instant provisioning, branching.
Drizzle ORM
Type-safe SQL. Schema in TypeScript, migrations, zero bloat.
Next.js
Server Components fetch data directly. No API layer needed.
Before You Start
- ✓A Next.js app (follow Your First App if needed)
- ✓A Neon account — free at neon.tech
Create a Neon Database
Sign in to console.neon.tech and create a new project.
- 1. Click "New Project"
- 2. Name it (e.g., "my-app-db")
- 3. Choose a region close to you
- 4. Click "Create Project"
After creation, you'll see your connection string. It looks like this:
postgresql://username:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=requireKeep this secret!
Never commit database URLs to git. We'll store it in an environment variable.
Install Dependencies
In your Next.js project, install Drizzle and the Neon adapter:
npm install drizzle-orm @neondatabase/serverless dotenvAnd the Drizzle Kit for migrations (dev dependency):
npm install -D drizzle-kitSet Up Environment Variables
Create a .env.local file in your project root:
DATABASE_URL="postgresql://username:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"Paste your actual connection string from Neon here.
Don't forget .gitignore
Next.js already ignores .env.local by default. Double-check it's in your .gitignore.
Create Database Connection
Create a new file at lib/db.ts:
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);That's it! You now have a db object ready for queries.
Define Your Schema
Create lib/schema.ts to define your tables:
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// TypeScript type for a post
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;Why Drizzle?
Your schema IS your TypeScript types. No code generation, no sync issues. When you change the schema, TypeScript catches errors instantly.
Configure Drizzle Kit
Create drizzle.config.ts in your project root:
import { defineConfig } from "drizzle-kit";
import * as dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
export default defineConfig({
schema: "./lib/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});Push Schema to Database
Run this command to create your tables in Neon:
npx drizzle-kit pushYou should see output confirming the posts table was created.
Other Drizzle Commands
npx drizzle-kit studio— Visual DB browsernpx drizzle-kit generate— Generate migration filesnpx drizzle-kit migrate— Run migrations
Query Data in Your App
Now you can fetch data directly in Server Components! Update your app/page.tsx:
import { db } from "@/lib/db";
import { posts } from "@/lib/schema";
export default async function Home() {
// This runs on the server - no API needed!
const allPosts = await db.select().from(posts);
return (
<main className="min-h-screen p-8 bg-black text-white">
<h1 className="text-4xl font-bold mb-8">My Posts</h1>
{allPosts.length === 0 ? (
<p className="text-gray-400">No posts yet.</p>
) : (
<ul className="space-y-4">
{allPosts.map((post) => (
<li key={post.id} className="p-4 bg-gray-900 rounded-lg">
<h2 className="text-xl font-bold">{post.title}</h2>
<p className="text-gray-400">{post.content}</p>
</li>
))}
</ul>
)}
</main>
);
}No "use client" needed!
Server Components can await database calls directly. The data fetches on the server, and the HTML ships to the browser.
Add Some Test Data
Create a simple API route to add posts. Create app/api/posts/route.ts:
import { db } from "@/lib/db";
import { posts } from "@/lib/schema";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { title, content } = await request.json();
const newPost = await db.insert(posts).values({
title,
content,
}).returning();
return NextResponse.json(newPost[0]);
}Test it with curl or your browser's dev tools:
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "content": "My first post!"}'Refresh your homepage—your post should appear!
Database Connected!
You're now fetching real data from Postgres. No Firebase, no Supabase client—just SQL with full TypeScript safety.
Final File Structure
Common Issues
"DATABASE_URL is not defined"
Make sure .env.local exists and restart the dev server after creating it.
"relation does not exist"
You haven't pushed the schema yet. Run npx drizzle-kit push
"Can't reach database"
Check your connection string. Neon projects "sleep" after 5 mins of inactivity but wake automatically.
Types not working?
Make sure you're importing from @/lib/schema (with the @ alias). Check tsconfig.json has path aliases set up.
Quick Query Reference
Select All
const all = await db.select().from(posts);Select with Filter
import { eq } from "drizzle-orm";
const post = await db.select().from(posts).where(eq(posts.id, 1));Insert
await db.insert(posts).values({ title: "New Post", content: "..." });Update
await db.update(posts).set({ title: "Updated" }).where(eq(posts.id, 1));Delete
await db.delete(posts).where(eq(posts.id, 1));Next Steps
Database Deep Dive
Branching, connection pooling, and advanced Neon features.
Read PlaybookAdd Authentication
Protect your data with user accounts and login flows.
Read PlaybookDeploy with Database
Set up environment variables on Vercel for production.
Read PlaybookDrizzle Docs
Full documentation for advanced queries, relations, and more.
Visit Site