Server Components and Server Actions run on the server, so they can talk to a database directly with no API layer in between. What you do need is a client that survives hot reloads, stays out of client bundles and is organised so every query passes through one place. After this lesson you will be able to set up Prisma, Drizzle or Mongoose in a Next.js project and structure a data access layer around it.
globalThis.server-only in the database module so a stray client import fails the build.lib/ functions that pages and actions call; authorisation checks belong there too.Install and initialise:
npm install prisma @prisma/client @prisma/adapter-pg
npx prisma initDefine a model in prisma/schema.prisma, then run npx prisma migrate dev --name init to create the table and regenerate the client:
model Todo {
id String @id @default(cuid())
title String
done Boolean @default(false)
createdAt DateTime @default(now())
}The singleton in the sample at the top of this lesson uses the PostgreSQL driver adapter that current Prisma versions require. Queries such as prisma.todo.findMany() are typed from the schema, and a Server Action can call prisma.todo.create() followed by revalidatePath().
Drizzle keeps the schema in TypeScript and generates SQL you can read:
// db/schema.ts
import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";
export const todos = pgTable("todos", {
id: text("id").primaryKey(),
title: text("title").notNull(),
done: boolean("done").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});// db/index.ts
import "server-only";
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";
export const db = drizzle(process.env.DATABASE_URL!, { schema });Push the schema with npx drizzle-kit push (or generate and migrate for versioned migrations) and query with db.select().from(todos) or db.query.todos.findMany().
MongoDB connections must also be cached across reloads. The official pattern stores the promise, not just the connection:
// lib/mongoose.ts
import "server-only";
import mongoose from "mongoose";
const cached = (globalThis as any).mongoose ?? { conn: null, promise: null };
(globalThis as any).mongoose = cached;
export async function connectDB() {
if (cached.conn) return cached.conn;
cached.promise ??= mongoose.connect(process.env.MONGODB_URI!);
cached.conn = await cached.promise;
return cached.conn;
}Call await connectDB() at the start of each DAL function, define models with mongoose.models.Todo ?? mongoose.model("Todo", schema) to avoid re-registration errors, and add serverExternalPackages: ["mongoose"] to next.config.ts.
| | Prisma | Drizzle | Mongoose |
|---|---|---|---|
| Database | SQL (Postgres, MySQL, SQLite, more) | SQL | MongoDB |
| Schema | .prisma DSL | TypeScript | JavaScript schemas |
| Query style | Object API | SQL-like builder | Document API |
| Best for | Fast start, rich tooling | Full SQL control, small bundles | Document data models |
Why cache the database client on `globalThis` in development?
globalThis (Prisma, Mongoose) so development reloads do not exhaust connections.server-only and never return raw records to the client.Next lesson: Authentication and Authorization Patterns ā sessions, protected routes and role checks in the App Router.