Handbooks / Next.js / Chapter 11
Database Access and Internationalization
7 pages · ~5 min✓ Reviewed
Builds on Cookies, Headers and Runtimes.
Server-Side Data Access: The Mental Model
1. Query in the component
- Server Components are async functions
awaitthe DB call in the body: no API route, nouseEffect- DB code runs only on the server
- Credentials and queries never reach the client bundle
2. Fetch where it is used
- Query in the component that needs the data
- Result is serialised into the RSC payload
- Client Components get only plain serialisable values
3. Static or dynamic
| Component reads | Rendering | DB query runs |
|---|---|---|
cookies(), headers() | Dynamic | each request |
| no request data | Static | at build time |
4. Parallel queries
- Independent queries: start together with Promise.all
- Sequential
awaits create a waterfall
const [user, posts] = await Promise.all([ db.user.find(id), db.post.list(id), ]);
Each await waits for the previous one. Total time = sum of queries, not the slowest.
Prisma and Drizzle Workflow
1. Prisma: Schema to Query
- schema.prismadefine models
- prisma generatebuild typed client
- Queryprisma.user.findMany()
const users = await prisma.user.findMany()
typed from the models in schema.prisma
2. Drizzle: Tables in TypeScript
export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name'), }) const rows = await db.select().from(users) .where(eq(users.id, id))
schema is plain TS; the query reads like SQL
3. Schema Changes
| Prisma | Drizzle | |
|---|---|---|
| Create | prisma migrate dev (development) | drizzle-kit generate makes SQL |
| Apply | prisma migrate deploy (production) | drizzle-kit migrate |
Connection Reuse in Development
1. Why Dev Leaks Connections
- Hot reload re-runs module code
- Each reload makes a new
PrismaClientand a new connection pool - Symptom: too many connections error during
next dev
- Save file
- Module re-runs
- New client + pool
- Too many connections
2. Cache on globalThis
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient } export const prisma = globalForPrisma.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
globalThis survives hot reloads
- Reuse the cached client if it exists
- Cache only when
NODE_ENVis not production
3. Drizzle
| Driver | globalThis cache? |
|---|
Keeping DB Code Server-Only
1. Server-Only Guard
import 'server-only'fails the build if a Client Component imports it- Keep queries in a data access layer:
lib/data.ts - Components import only that file
// lib/data.ts import 'server-only' export async function getUser(id) { const u = await db.user.findUnique({ where: { id } }) return { id: u.id, name: u.name } }
returns a DTO, not the whole row
2. Secrets and DTOs
| Value | Reaches browser? |
|---|---|
NEXT_PUBLIC_* env | Yes |
DATABASE_URL | Never, no prefix |
| Whole DB row | No, return a DTO |
- A DTO holds only the needed fields
- Leaving out passwordHash keeps it off the client
i18n Routing with [lang] and Dictionaries
1. The [lang] Segment
- Pages live in app/[lang]/: the locale is a dynamic segment
- Read it from the
paramsprop:/en/aboutgiveslang = 'en' - Recent Next.js:
paramsis a Promise, so await it generateStaticParamsreturns the locales, so each language pre-renders
2. Middleware Redirect
- Request/about
- Accept-Languageread header
- Negotiatesupported list
- Redirect/en/about
- Path has no locale: redirect to the negotiated one (default
/en/...) - Matcher must exclude
_next, static files andapi:'/((?!_next|api|.*\..*).*)'
3. Dictionaries
- One JSON file per locale, loaded with dynamic import
- Loader wrapped in
server-only; call it in Server Components, pass strings down as props
Data Access and i18n Cheat Sheet
1. Query and Migrate
const rows = await db.select().from(t)
Server Component body: no API layer needed
| Generate | Apply | Prototype | |
|---|---|---|---|
| Prisma | prisma generate | prisma migrate dev / prisma migrate deploy (prod) | prisma db push (no migration files) |
| Drizzle | drizzle-kit generate | drizzle-kit migrate | drizzle-kit push |
2. Dev Singleton and Guards
const g = globalThis as any export const prisma = g.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') g.prisma = prisma
cached only when NODE_ENV is not production
import 'server-only'in DB modules- No
NEXT_PUBLIC_on secrets
Check yourself
lib/db.ts has export const prisma = new PrismaClient(). After many saves in next dev, the database says "too many connections". Why?
- Each hot reload re-runs the module, so it builds a new client and pool.
- Fix: cache it on
globalThis, only whenNODE_ENVis not production.
You set NEXT_PUBLIC_DATABASE_URL so a Client Component can read it. What is the bug?
- The
NEXT_PUBLIC_prefix inlines the value into the browser bundle, so the credentials leak. - Keep
DATABASE_URLunprefixed and query only in server-only code.
Your locale middleware has no matcher. A request for /_next/static/app.js arrives. What happens?
- It has no locale, so it is redirected to
/en/_next/static/app.js, which returns 404. - The matcher must exclude
_next, static files and api routes.