Next.js
Integración con PostgreSQL y ORM
Explica cómo integrar PostgreSQL y un ORM con Next.js usando DAL, pooling, constraints, índices, transacciones, migraciones y aislamiento por tenant.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo integrar PostgreSQL y un ORM con Next.js usando DAL, pooling, constraints, índices, transacciones, migraciones y aislamiento por tenant.
Next.js puede acceder a PostgreSQL desde Server Components, Actions, Route Handlers, DAL y jobs, nunca desde Client Components. El ORM simplifica consultas y tipos, pero no sustituye modelado, constraints, índices, transacciones, migraciones, pooling ni autorización por tenant.
request
→ authenticate principal
→ validate input
→ authorized DAL/service
→ connection pool
→ PostgreSQL transaction/query
→ DTO
→ render/responseLa query debe incorporar scope antes de devolver filas.
// server/db.ts
import "server-only";
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const db = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;Este patrón evita múltiples clients durante Fast Refresh; revisa recomendación del ORM/runtime. En serverless, la plataforma puede crear muchas instancias, por lo que necesitas pooling externo o driver compatible.
PostgreSQL limita conexiones. Serverless puede escalar funciones más rápido que DB:
100 function instances × 10 connections
→ 1000 connections
→ database exhaustedOpciones:
Comprueba modo transaction/session y compatibilidad con prepared statements.
Coloca aplicación y DB cerca. Edge global + DB única distante puede ser más lento que Node en la misma región. Mide round trips; una consulta de 5 ms repetida diez veces a 100 ms de red es un problema de arquitectura.
const order = await db.order.findFirst({
where: {
id: orderId,
organizationId: session.organizationId,
},
select: {
id: true,
status: true,
total: true,
createdAt: true,
},
});Selecciona campos mínimos y scope de tenant. No cargues relaciones completas para mapearlas después.
1 query orders
+ 100 queries customerSoluciones:
No uses include de todo como solución universal; puede producir explosión cartesiana.
Un índice debe seguir patrones reales:
CREATE INDEX orders_org_status_created_idx
ON orders (organization_id, status, created_at DESC);Evalúa con EXPLAIN ANALYZE. Más índices aceleran lecturas pero aumentan escritura, almacenamiento y mantenimiento.
Índice único también expresa integridad:
UNIQUE (organization_id, external_id)La aplicación valida para UX; DB protege integridad concurrente:
No confíes solo en un findFirst previo: dos requests pueden pasar simultáneamente.
await db.$transaction(async (tx) => {
const order = await tx.order.create({ data });
await tx.stock.updateMany({
where: { productId, quantity: { gte: requested } },
data: { quantity: { decrement: requested } },
});
await tx.outbox.create({ data: toOrderCreatedEvent(order) });
});Mantén la transacción corta. No llames email/payment lento dentro. Usa outbox después del commit.
Patrones:
SELECT ... FOR UPDATE bajo necesidad.No uses locks amplios sin medir contención/deadlocks.
Proceso seguro:
expand schema
→ deploy compatible code
→ backfill
→ switch reads/writes
→ remove old column laterEvita renombrar/eliminar una columna en el mismo deploy cuando instancias viejas aún corren. Prueba migraciones con volumen representativo y backups.
No ejecutes migraciones destructivas automáticamente desde cada instancia al arrancar.
Productividad, relaciones y tipos; puede ocultar SQL costoso.
Más control con tipos.
Máximo control y capacidades PostgreSQL; mayor responsabilidad.
Combinar es válido. Revisa SQL generado y planes.
Row-Level Security puede añadir defensa:
CREATE POLICY tenant_isolation ON orders
USING (organization_id = current_setting('app.organization_id')::uuid);Requiere configurar contexto por transacción/conexión correctamente, especialmente con pooling. No reemplaza validación de acción ni DTOs.
Cachea DTOs públicos o compartibles, no handles/clientes ORM. Invalida después del commit. Para datos privados, key y permisos deben mantener aislamiento.
Una replica puede tener lag; read-after-write puede requerir primary o estrategia de consistencia.
Mapea códigos:
No expongas SQL ni schema.
Un backup no comprobado no es recuperación garantizada.
Expone credenciales/control.
Agota conexiones.
Race condition.
Payload/joins enormes.
Escrituras más lentas.
Rompe deploy rolling.
Añade latencia sin necesidad.
Fuga.
Runtime de Node.js y Edge runtime determina qué drivers y capacidades pueden ejecutar esta integración.