Back to Playbook
GuideEssential

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

1

Create a Neon Database

Sign in to console.neon.tech and create a new project.

  1. 1. Click "New Project"
  2. 2. Name it (e.g., "my-app-db")
  3. 3. Choose a region close to you
  4. 4. Click "Create Project"

After creation, you'll see your connection string. It looks like this:

env
postgresql://username:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require

Keep this secret!

Never commit database URLs to git. We'll store it in an environment variable.

2

Install Dependencies

In your Next.js project, install Drizzle and the Neon adapter:

bash
npm install drizzle-orm @neondatabase/serverless dotenv

And the Drizzle Kit for migrations (dev dependency):

bash
npm install -D drizzle-kit
3

Set Up Environment Variables

Create a .env.local file in your project root:

.env.local
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.

4

Create Database Connection

Create a new file at lib/db.ts:

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.

5

Define Your Schema

Create lib/schema.ts to define your tables:

lib/schema.ts
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.

6

Configure Drizzle Kit

Create drizzle.config.ts in your project root:

drizzle.config.ts
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!,
  },
});
7

Push Schema to Database

Run this command to create your tables in Neon:

bash
npx drizzle-kit push

You should see output confirming the posts table was created.

Other Drizzle Commands

  • npx drizzle-kit studio — Visual DB browser
  • npx drizzle-kit generate — Generate migration files
  • npx drizzle-kit migrate — Run migrations
8

Query Data in Your App

Now you can fetch data directly in Server Components! Update your app/page.tsx:

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.

9

Add Some Test Data

Create a simple API route to add posts. Create app/api/posts/route.ts:

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:

bash
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

my-app/
.env.local← DATABASE_URL
drizzle.config.ts
lib/
db.ts← Connection
schema.ts← Table definitions
app/
page.tsx← Fetches posts
api/posts/route.ts← POST endpoint

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

typescript
const all = await db.select().from(posts);

Select with Filter

typescript
import { eq } from "drizzle-orm";
const post = await db.select().from(posts).where(eq(posts.id, 1));

Insert

typescript
await db.insert(posts).values({ title: "New Post", content: "..." });

Update

typescript
await db.update(posts).set({ title: "Updated" }).where(eq(posts.id, 1));

Delete

typescript
await db.delete(posts).where(eq(posts.id, 1));