Databases and data boundaries
Server Components can query a database directly. A clean data boundary keeps that convenience from spreading query details and permission mistakes across the component tree.
Skip an internal HTTP hop when the server owns both sides
A Server Component already runs on the server, so it can call a database function directly. Fetching your own Route Handler adds serialization, another error surface, and an HTTP round trip inside the same application.
Keep Route Handlers for real HTTP consumers such as mobile clients, webhooks, and public integrations. Share domain functions beneath both the page and the handler when they need the same operation.
Put permission-aware reads in a data access layer
A useful data function accepts the authenticated actor and the resource identifier, checks access, selects only the fields the caller needs, and returns a plain result. This makes the safe path the easy path.
Avoid returning a full database row to every component. An internal notes field or password hash can leak later when somebody spreads the object into a Client Component or JSON response.
import 'server-only'
export async function getProjectSummary(userId: string, projectId: string) {
const project = await db.query.projects.findFirst({
where: and(eq(projects.id, projectId), eq(projects.ownerId, userId)),
columns: { id: true, name: true, updatedAt: true },
})
if (!project) return null
return project
}Match connection strategy to the runtime
A long-running Node server, a serverless function, and an edge runtime have different connection limits and driver support. Use a database provider and driver designed for the environment where the query runs.
During local development, hot reload can recreate modules repeatedly. Follow the driver guidance for reusing clients so a small edit does not open a new pool until the database refuses connections.
Use a transaction when partial success would be wrong
Creating an order and decrementing inventory are one business operation even if they touch several tables. Put dependent writes in a transaction so either all of them commit or none do.
External calls cannot usually join the database transaction. Record durable intent, commit it, and let a retryable worker perform email, payment, or webhook work. This is more reliable than holding a database transaction open during a slow network call.
Observe queries before guessing about performance
Log slow query duration and inspect execution plans for important paths. Indexes help reads but add storage and write cost, so add them for actual access patterns rather than every column that looks searchable.
Pagination should have a stable order. For large or frequently changing collections, cursor pagination usually behaves more predictably than a deep offset.
Can you explain “Databases and data boundaries” without reaching for the API reference?
Describe the main trade-off, one failure mode, and the decision you would make in a real project.