Database Access with Prisma, Drizzle and Mongoose

Intermediate
14 min

Database Access with Prisma, Drizzle and Mongoose

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.

Rules That Apply to Every Database

  • One client per process. Development reloads modules on every change; a new client each time exhausts connection limits. Cache the instance on globalThis.
  • Server only. Import server-only in the database module so a stray client import fails the build.
  • A data access layer. Put queries in lib/ functions that pages and actions call; authorisation checks belong there too.
  • Pooling on serverless. Many short-lived instances each open connections; use a pooled connection string or a pooling service.

Prisma

Install and initialise:

bash
npm install prisma @prisma/client @prisma/adapter-pg npx prisma init

Define a model in prisma/schema.prisma, then run npx prisma migrate dev --name init to create the table and regenerate the client:

text
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

Drizzle keeps the schema in TypeScript and generates SQL you can read:

typescript
// 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(), });
typescript
// 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().

Mongoose

MongoDB connections must also be cached across reloads. The official pattern stores the promise, not just the connection:

typescript
// 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.

Choosing

| | 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 |

Common mistakes

  • Running migrations from application code at request time; run them in CI or a deploy step.
  • Returning full database records to the client, including password hashes; map to a DTO first.
Quick Quiz
Question 1 of 3

Why cache the database client on `globalThis` in development?

Key Takeaways

  • Query the database directly from Server Components and Server Actions through a data access layer.
  • Cache clients on globalThis (Prisma, Mongoose) so development reloads do not exhaust connections.
  • Mark database modules with server-only and never return raw records to the client.
  • Prisma: schema DSL and generated client; Drizzle: TypeScript schemas and SQL-like queries; Mongoose: MongoDB.
  • Use pooled connections on serverless platforms and run migrations at deploy time.

Next lesson: Authentication and Authorization Patterns — sessions, protected routes and role checks in the App Router.

Database Access with Prisma, Drizzle and Mongoose - Next.js | CodeYourCraft | CodeYourCraft