Next.js Connecting to a Database

A database stores your application's data — user accounts, blog posts, orders, or any information that persists beyond a single page load. Next.js connects to databases entirely on the server. Your database credentials never reach the browser, and your database connection stays secure.

Where Database Code Lives

Database queries run only in server-side code. They belong in server components, server actions, and API routes — never in client components.

SERVER SIDE (safe for DB):
  ✓ Server Components (async page.js, layout.js)
  ✓ Server Actions ('use server' functions)
  ✓ API Routes (route.js files)

CLIENT SIDE (no DB access):
  ✗ Client Components ('use client')
  ✗ useEffect hooks
  ✗ Browser JavaScript

Popular Database Options

TYPE            EXAMPLE SERVICES           BEST FOR
──────────────────────────────────────────────────────────
SQL             PostgreSQL, MySQL          Structured data, relations
NoSQL           MongoDB, DynamoDB          Flexible data, documents
Cloud SQL       Supabase, PlanetScale      SQL with built-in APIs
SQLite          libSQL (Turso)             Simple apps, prototypes

Option 1: Using Prisma (Recommended for SQL)

Prisma is an ORM (Object-Relational Mapper). Instead of writing raw SQL, you write JavaScript. Prisma translates your JavaScript into SQL queries and handles the database connection for you.

Install Prisma

npm install prisma @prisma/client
npx prisma init

Define Your Schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  createdAt DateTime @default(now())
}

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
  posts Post[]
}

Generate the Client and Run Migrations

npx prisma migrate dev --name init
npx prisma generate

Create the Database Client File

// app/lib/db.js
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis;

const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

export default prisma;

This pattern prevents creating too many database connections during development when Next.js hot-reloads your files.

Diagram: Why Global Client Matters in Dev

WITHOUT GLOBAL PATTERN (development):
File saves → Next.js hot reloads → New PrismaClient created
File saves → Hot reload → Another new PrismaClient
File saves → Hot reload → Another new PrismaClient
Result: 100s of connections → Database overwhelmed

WITH GLOBAL PATTERN:
File saves → Next.js hot reloads → Checks globalThis.prisma
                                  → Already exists → Reuse it
Result: One connection → Database happy

Using Prisma in a Server Component

// app/blog/page.js
import prisma from '@/app/lib/db';

export default async function BlogPage() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Using Prisma in a Server Action

// app/actions.js
'use server';

import prisma from '@/app/lib/db';
import { revalidatePath } from 'next/cache';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');

  await prisma.post.create({
    data: { title, content, published: true }
  });

  revalidatePath('/blog');
}

Option 2: Using MongoDB with Mongoose

npm install mongoose
// app/lib/mongoose.js
import mongoose from 'mongoose';

let isConnected = false;

export async function connectDB() {
  if (isConnected) return;

  await mongoose.connect(process.env.MONGODB_URI);
  isConnected = true;
}
// app/lib/models/Post.js
import mongoose from 'mongoose';

const PostSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: String,
  createdAt: { type: Date, default: Date.now },
});

export const Post = mongoose.models.Post || mongoose.model('Post', PostSchema);
// Usage in a server component or action:
import { connectDB } from '@/app/lib/mongoose';
import { Post } from '@/app/lib/models/Post';

await connectDB();
const posts = await Post.find({}).sort({ createdAt: -1 });

Option 3: Supabase (PostgreSQL as a Service)

Supabase provides a hosted PostgreSQL database with a simple JavaScript client. It also includes built-in authentication and file storage.

npm install @supabase/supabase-js
// app/lib/supabase.js
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.SUPABASE_SERVICE_KEY   // Use service key server-side only
);
// Fetch data from Supabase in a server component
const { data: posts, error } = await supabase
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false });

Diagram: The Full Data Flow

BROWSER
  ↓ (HTTP request)
NEXT.JS SERVER
  ↓ (server component or action runs)
DATABASE CLIENT (Prisma / Mongoose / Supabase)
  ↓ (query)
DATABASE (PostgreSQL / MongoDB / Supabase)
  ↓ (data returned)
SERVER renders HTML with data
  ↓ (HTML sent)
BROWSER displays the page

Key Takeaway

Database code belongs on the server — in server components, server actions, and API routes. Use Prisma for SQL databases with type-safe queries and automatic migrations. Use Mongoose for MongoDB. Use Supabase for a managed PostgreSQL database with minimal setup. Always create a single, reusable database client instance to avoid connection overload, especially in development.

Leave a Comment