Creación de una API GraphQL segura desde el punto de vista tipológico con Prisma y Nexus en Node.js
Sigue una guía de siete pasos para crear una API GraphQL de Node.js que unifique el modelo de datos de Prisma con los tipos y resolvers generados por Nexus.
Aprenda cómo integrar Prisma Nexus en un proyecto Node.js para crear APIs GraphQL seguras desde el punto de vista tipológico, abordando el diseño del esquema, la lógica de los resolvers y un servidor en funcionamiento.
Imagínese un proyecto GraphQL donde el mismo tipo “User” está definido en cuatro lugares diferentes: un documento SDL, una interfaz de TypeScript escrita a mano, un modelo Prisma y un validador Zod que un compañero de equipo añadió meses después del lanzamiento. Cada vez que se modifica una de esas definiciones, al menos otra deja de estar sincronizada. Se publica una corrección y, de repente, los tipos de TypeScript siguen asumiendo que phone es obligatorio, aunque esa columna desapareció de la base de datos semanas antes.
Ese tipo de desviación es exactamente lo que se pretende evitar al combinar Prisma con Nexus. Nexus crea tu esquema GraphQL y tus tipos de TypeScript directamente a partir del mismo modelo de datos que ya definiste en Prisma. Existe una única fuente de verdad, y todo lo demás se deriva de ella. Al actualizar la definición una sola vez, los tipos, el esquema y las firmas de los resolvers se actualizan al mismo tiempo. Suena como sentido común una vez que lo dices en voz alta; la verdadera lección proviene de trabajar sin él y experimentar cuán costoso se vuelve ese vacío.
Esta guía explica cómo crear una API GraphQL de Node.js desde cero con Prisma y Nexus, dividida en siete pasos con código completo y sin omitir nada. Al final tendrás un servidor funcional conectado a PostgreSQL, algo que puedes ejecutar, expandir y utilizar con confianza. Está diseñada como una base lo suficientemente sólida para un backend de comercio electrónico en producción real, y no como una demostración que falla en cuanto se agrega un segundo modelo.
Qué tener listo antes del paso 1
Necesitarás:
- Node.js instalado: descarga la versión LTS más reciente desde nodejs.org si aún no la tienes.
- La CLI de Prisma disponible globalmente:
npm install -g prisma
- Una base de datos PostgreSQL en ejecución a la que puedas acceder. Puede ser un contenedor Docker local, el plan gratuito de Supabase o Railway; no importa el hosting, siempre y cuando tengas una cadena de conexión a mano.
Una nota para quienes apliquen esto a un código existente en lugar de a un proyecto nuevo: durante la primera migración, Prisma intenta reconciliar schema.prisma con lo que ya existe en la base de datos. En un esquema heredado desorganizado, ese paso de reconciliación puede generar una diferencia grande e intimidante. Revísela cuidadosamente antes de aplicarla y siempre pruébela primero en un entorno de desarrollo. Si comienzas desde cero, nada de esto se aplica a ti por ahora.
Paso 1: Poner en marcha el proyecto
Este es el paso más rápido de todo el proceso. Crea una carpeta e incluye todas las dependencias de una sola vez:
mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path
Esa única orden instala los siete paquetes al mismo tiempo: el entorno de ejecución de GraphQL, Nexus para la creación de esquemas basados en código, Prisma en sí mismo, y la combinación Apollo/Express que hará funcionar el servidor. Instalar todo junto no es solo cuestión de conveniencia; permite que npm resuelva las dependencias entre paquetes en un solo proceso, en lugar de arriesgarse a versiones menores incompatibles si se instalan los paquetes uno por uno.
Paso 2: Conectar Prisma a su base de datos
npx prisma init
Responda a las preguntas y elija PostgreSQL. Una vez que finalice la orden, aparecerán dos archivos nuevos que antes no estaban:
prisma/schema.prisma— aquí se encuentra su modelo de datos.env— aquí se coloca la cadena de conexiónDATABASE_URL, y debe colocarse allí de inmediato
Eso no es una exageración. Antes de tocar el esquema, antes de ejecutar una migración, antes de abrir cualquier otra cosa, coloque su cadena de conexión en .env. A partir de este momento, prácticamente cada comando de Prisma intentará conectarse a la base de datos, y los errores que aparecen cuando la cadena falta o está mal formada son notoriamente poco útiles. En lugar de un mensaje claro de “cadena de conexión inválida”, recibirá una queja vaga sobre el hecho de que el cliente no está inicializado, y fácilmente podrá perder quince minutos buscando la causa incorrecta.
Paso 3: Escribir el esquema de Prisma — Esto no es su esquema GraphQL
Si ya has trabajado con GraphQL anteriormente pero nunca junto a Prisma, evita tratar schema.prisma como el lugar donde diseñas la interfaz de tu API. No es así; se trata de una representación de la estructura de tu base de datos: tablas, columnas, relaciones y restricciones. La forma real de la API se deriva posteriormente a partir de esto, mediante Nexus. Mantén esta distinción, ya que ayuda a mantener coherente todo el modelo mental.
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
Una vez que hayas escrito tu modelo, ejecuta la migración:
npx prisma migrate dev
This single command does two things nothing else in the setup does: it creates the actual table in your database, and it regenerates Prisma Client with TypeScript types that exactly match your current schema. Skip it, and Prisma Client simply won't recognize that a User model exists. What you get instead are type errors buried in generated files you don't control, with call stacks that lead nowhere useful — there's no clever shortcut around that. Run the migration every time your schema changes, without exception.
Step 4: Nexus — Why One More File Is Worth It
En esta etapa de la configuración, es lógico preguntarse si Nexus realmente está cumpliendo con su función. Nada impide crear un servidor GraphQL sin él: escribe a mano el SDL, define tus propias interfaces en TypeScript y conecta todo manualmente a los resolvers. Muchos proyectos de código hacen exactamente eso. El problema es que este enfoque abre la puerta a un tipo específico de error: el SDL indica una forma determinada, los tipos de TypeScript describen otra ligeramente diferente, y el resolver devuelve algo completamente distinto. Determinar cuál de las tres versiones es la “real” suele llevar más tiempo que desarrollar la función en sí desde el principio.
Nexus evita ese problema al tratar el SDL como un resultado generado en lugar de algo que se escribe a mano. Usted describe sus tipos en TypeScript, y Nexus deriva tanto el SDL como las definiciones de tipo correspondientes a partir de esa única fuente. Las tres partes que antes podían desviarse se convierten en un único artefacto que estructuralmente no puede contradecirse a sí mismo. Así es como se ve schema.ts:
// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';const schema = makeSchema({
types: [resolvers],
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus.ts'),
},
});export default schema;
La configuración outputs indica a Nexus dónde colocar los archivos que genera: generated/schema.graphql recibe el SDL, y generated/nexus.ts recibe las definiciones correspondientes en TypeScript. Ambos se vuelven a escribir en cada ejecución, por lo que nunca deberías modificarlos manualmente. Si abres generated/nexus.ts y notas algo que necesita corrección, resiste la tentación de editarlo directamente; en su lugar, localiza la definición original y cámbiala allí. Modificar un archivo generado es algo similar a aplicar parches a un binario compilado: funciona hasta que la siguiente compilación borra silenciosamente tus cambios.
Paso 5: Resolvers — Conectando el esquema a la base de datos
// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id')
t.string('name')
t.string('email')
},
})export const Query = extendType({
type: 'Query',
definition(t) {
t.list.field('users', {
type: 'User',
resolve: async () => {
return await prisma.user.findMany();
},
});
},
});export const Mutation = extendType({
type: 'Mutation',
definition(t) {
t.field('createUser', {
type: 'User',
args: {
name: nonNull(stringArg()),
email: nonNull(stringArg()),
},
resolve: async (_, args) => {
return await prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
});
},
});
Tenga en cuenta que PrismaClient se instancia una sola vez, en el nivel más alto del módulo, fuera del cuerpo de cualquier función. Esa ubicación es más importante de lo que podría parecer a primera vista. Cada llamada a new PrismaClient() abre una conexión nueva al banco de datos. Si se creara dentro de un resolver, se establecería una conexión nueva con cada solicitud. Durante el desarrollo local normal, con quizás una o dos solicitudes por segundo, el banco de datos ni siquiera notará la diferencia. Pero bajo un tráfico real concurrente —imagine unos cientos de compradores accediendo a /checkout al mismo tiempo durante una promoción— ese patrón agotará el límite de conexiones de PostgreSQL y comenzará a generar errores bajo carga.
Declarar al cliente a nivel de módulo implica que todo el proceso comparte una única conexión. Las solicitudes no compiten por abrir sus propias conexiones a la base de datos; se encolan en una cola frente a un cliente compartido, el cual gestiona internamente su propio pool de conexiones. Este es el tipo de detalle que los desarrolladores experimentados en Node.js aplican de forma automática, mientras que los equipos menos experimentados tienden a descubrirlo a la fuerza, en medio de un incidente. Ahora puedes saltarte esa lección.
Paso 6: El servidor
// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';const app = express();
const server = new ApolloServer({ schema });const startServer = async () => {
await server.start(); // Start Apollo Server server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express const PORT = process.env.PORT || 4000; app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
}startServer().catch((err) => {
console.error('Error starting the server:', err);
});
Un detalle importante a tener en cuenta antes de comenzar: await server.start() debe ejecutarse antes que server.applyMiddleware(). Este requisito de ordenación no existía en Apollo Server 2; Apollo 3 introdujo una fase de inicio asíncrono explícita, y cualquier código de ejemplo escrito antes de finales de 2021 probablemente carezca por completo de esta llamada. Si la omite, aparecerá el error Server must be started before calling server.applyMiddleware, lo cual al menos deja claro qué salió mal, aunque no explica por qué existe esa regla. Una vez que comprenda el motivo, se trata de una solución de dos segundos en lugar de un desvío confuso.
Paso 7: Inícielo. Rompálo. Confíe en él.
node server.ts
Vaya a http://localhost:4000/graphql. Esto lo llevará al GraphQL Playground. Ejecute primero la mutación:
// Fetch Users
query {
users {
id
name
email
}
}
// Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
Ejecuta la mutación antes de la consulta, para que realmente haya datos que obtener. Observa cómo el registro que acabas de insertar aparece en la respuesta de la consulta. Luego haz algo que la mayoría de las guías omiten: abre un cliente de base de datos — psql, TablePlus, DBeaver, lo que tengas — e inspecciona directamente la tabla User. No el JSON que devolvió la API, sino la tabla en sí, sin procesar.
Tu fila está allí, creada mediante una mutación GraphQL que definiste en TypeScript usando tipos de Nexus, ejecutada a través de Prisma y persistida en PostgreSQL. Cada eslabón de esa cadena funcionó correctamente. Puedes señalar el lugar exacto donde el código de tu aplicación interactúa con la base de datos. Para quienes vienen de años utilizando endpoints REST y SQL escrito a mano, ese suele ser el momento en que esta tecnología deja de parecer un diagrama y comienza a sentirse como algo real.
Lo que has construido y lo que aún necesitas agregar
Lo que tiene ahora es una base de backend funcional, no un ejemplo meramente ilustrativo. El patrón que acaba de seguir —definir un modelo Prisma, ejecutar una migración, agregar un objeto de tipo Nexus, escribir el resolvedor e integrarlo en el servidor Apollo/Express— es exactamente lo que repetirá para cada modelo adicional que introduzca. Ya sea Product, Order o Cart, los pasos no cambian, al igual que las garantías. Agregue una relación dentro de schema.prisma, ejecute migrate dev y luego implemente el resolvedor; sus tipos se actualizarán automáticamente. Esa sincronización automática es realmente la principal ventaja de esta configuración: ya no depende de la memoria para mantener alineados su esquema, sus tipos y sus resolvedores, ya que las herramientas se encargan de ello por usted.
Lo que falta de manera evidente hasta ahora son la autenticación, autorización, limitación de frecuencia y validación de entradas. Nexus garantiza que sus tipos estén correctos, pero no dice nada sobre quién tiene permiso para realizar cada operación. En la situación actual, la mutación createUser responderá sin problemas a cualquiera que pueda acceder al puerto 4000. Esto es aceptable mientras se desarrolla localmente, pero deja de serlo en cuanto la API sea accesible desde una URL real. Es necesario agregar un middleware de autenticación antes de que esto se utilice en cualquier entorno al que otras personas puedan acceder.
Para una cobertura más exhaustiva, consulte la documentación de Prisma sobre relaciones, filtrado y paginación, así como la documentación de Nexus sobre autorización a nivel de campo y escalares personalizados. Ambos conjuntos de documentos están bien organizados para poder leerse completo, en lugar de solo echar un vistazo cuando surge algún problema; una característica que debería ser más común en la documentación técnica.
Lecturas relacionadas
- Creación de agentes de IA seguros con LangChain Guardrails y middleware — Aprenda cómo funcionan las medidas de seguridad deterministas y basadas en modelos en LangChain para detectar fugas de PII, aplicar reglas empresariales y agregar pasos de aprobación humana en los agentes de IA.