R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 19 of 42Article16 min

Setting Up Your Database & Seeding

So far your Next.js pages can render UI. Real apps also need a database — users, invoices, products. Think of the database as a filing cabinet in the office basement, and your Server Components as staff who walk downstairs, pull a folder, and bring it back to the front desk.

Your app needs a place to store data

So far your Next.js pages can render UI. Real apps also need a database — users, invoices, products. Think of the database as a filing cabinet in the office basement, and your Server Components as staff who walk downstairs, pull a folder, and bring it back to the front desk.

In the App Router you almost always talk to the database from the server (Server Components, Server Actions, or Route Handlers) — never from the browser with your secret connection string.

Never put DATABASE_URL or API secrets in client components. Env vars without NEXT_PUBLIC_ stay on the server only.

Postgres or SQLite — pick what fits

Postgres is the usual choice for production (Vercel Postgres, Neon, Supabase, Railway). SQLite (often via Prisma + file DB or Turso) is great for local learning and small projects.

Real-life example: Postgres is a shared warehouse for a growing shop. SQLite is a single notebook on your desk — perfect for practice, limited when many cashiers write at once.

  • Postgres — cloud-friendly, great for multi-user apps
  • SQLite — zero setup locally, one file
  • Prisma, Drizzle, or raw SQL drivers all work with App Router
.env.local — connection string
# Never commit this file
DATABASE_URL="postgresql://user:password@host:5432/mydb?sslmode=require"

# Local SQLite example (Prisma)
# DATABASE_URL="file:./dev.db"
Query with @vercel/postgres
import { sql } from "@vercel/postgres";

export async function getInvoices() {
  const { rows } = await sql`
    SELECT id, customer_name, amount, status
    FROM invoices
    ORDER BY created_at DESC
  `;
  return rows;
}
Prisma-style client (same idea)
// lib/db.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

// Usage in a Server Component
// const invoices = await prisma.invoice.findMany();

Seed scripts — fill the cabinet before opening day

A seed script inserts starter rows so your dashboard is not empty on first run. You run it once (or when resetting local data): npm run seed.

Real-life example: Seeding is stocking shelves before a shop opens — empty shelves make demos boring and tests flaky.

Add a package.json script like "seed": "tsx scripts/seed.ts" and run it after migrations. Keep seed data boring and safe — no real customer emails.
scripts/seed.ts (simplified)
import { sql } from "@vercel/postgres";

async function seed() {
  await sql`
    CREATE TABLE IF NOT EXISTS invoices (
      id SERIAL PRIMARY KEY,
      customer_name TEXT NOT NULL,
      amount INT NOT NULL,
      status TEXT NOT NULL,
      created_at TIMESTAMP DEFAULT NOW()
    )
  `;

  await sql`
    INSERT INTO invoices (customer_name, amount, status)
    VALUES
      ('Asha Traders', 2500, 'paid'),
      ('Ravi Motors', 1800, 'pending'),
      ('Knowvora Labs', 4200, 'paid')
    ON CONFLICT DO NOTHING
  `;

  console.log("Seed complete");
}

seed().catch((err) => {
  console.error(err);
  process.exit(1);
});