Equilibrar la generación Prisma CRUD con un control deliberado de rutas
Aprenda cómo la generación basada en esquemas de los routers CRUD de Prisma puede eliminar el código genérico repetitivo, al mismo tiempo que mantiene las decisiones relacionadas con la confianza, el alcance y la exposición dentro del código de la aplicación.
Los puntos de extremo CRUD suelen reproducir simplemente los datos que ya contiene el esquema Prisma. El nombre de un modelo se convierte en un segmento de ruta, los campos escalares en validaciones de entrada y las llamadas de Prisma en métodos del controlador. Las relaciones añaden otra capa de análisis de las solicitudes recibidas y de configuración de lo que se devuelve.
Esta visión del tema se basa en una herramienta de código abierto desarrollada por su mantenedor, evaluada a partir de su documentación actual y de experimentos que se pueden reproducir por uno mismo, y no a partir de afirmaciones sobre su grado de uso generalizado.
Esa repetición resulta costosa precisamente porque parece inofensiva. Cada manejador escrito a mano que se copia representa un lugar adicional donde los valores por defecto de la paginación, los campos permitidos, el alcance por tenant y el manejo de errores pueden diferir silenciosamente de los demás.
prisma-generator-express se encarga de ese trabajo manual y lo integra en el paso prisma generate. Puede generar controladores para Express, Fastify o Hono. Un paquete complementario, prisma-guard, genera metadatos de validación y alcance sensibles a Prisma al mismo tiempo, y las definiciones de operaciones indican con precisión qué argumentos puede enviar cada tipo de llamante.
Lo que se obtiene al final no es una aplicación sin código. Es una aplicación con mucha menos complejidad en la capa de interfaces, y con un control mucho más claro sobre las decisiones que aún quedan por tomar.
Esa distinción es la parte importante. La generación debería encargarse de todo lo que el esquema en sí pueda describir completamente. La autenticación, las operaciones que se exponen, quiénes son los llamantes y cualquier política que requiera conocimientos más allá del esquema aún deben estar incluidas en el código de la aplicación.
Mueva el trabajo repetible a un único paso de generación
Una API generada parte de tres elementos conectados: el Prisma Client, los metadatos de protección y los propios enrutadores HTTP.
generator client {
provider = "prisma-client-js"
}
generator guard {
provider = "prisma-guard"
output = "../generated/guard"
enforceProjection = "true"
}generator express {
provider = "prisma-generator-express"
target = "express"
}
Al ejecutar npx prisma generate una vez, se regeneran los tres archivos cada vez que cambia el esquema o la configuración del generador.
El esquema sigue siendo la única fuente de verdad para su modelo de datos. Los archivos de enrutador generados son solo el resultado del proceso de compilación, nada más. La configuración de los enrutadores determina qué operaciones se montan realmente, y las estructuras de protección definen qué argumentos de Prisma puede utilizar un llamante específico.
Mantener esas preocupaciones separadas es mucho más útil que tratar el código generado como un sustituto de la arquitectura. Se introducen intencionadamente dos entradas distintas en todo el proceso: la generación basada en esquemas se encarga de las mecánicas repetibles, mientras que la política a nivel de aplicación gestiona las decisiones de confianza y la exposición de rutas.
Si alguna regla no puede expresarse fielmente a través del generador o de una estructura de protección, no intente forzarla en la configuración. Un manejo dedicado o una política aplicada en la base de datos constituyen un límite más claro que un parámetro declarativo que oculta silenciosamente cuál es su función real.
La generación también modifica qué es lo que se revisa realmente en la revisión de código. El CRUD escrito a mano induce a los revisores a verificar línea por línea la lógica de análisis y delegación repetida. El CRUD generado dirige esa inspección hacia una superficie mucho más reducida: el esquema de Prisma, las opciones del generador, los descriptores de rutas, las estructuras y todo aquel resolutor que establece el contexto confiable.
Eso no hace que el resultado generado sea menos importante; simplemente significa que editarlo directamente no es la forma adecuada de abordarlo. Si una ruta necesita correcciones, modifique la configuración que la genera y vuelva a crearla. Un parche manual insertado en un archivo de router generado puede desaparecer con el siguiente cambio de esquema, sin dejar rastro del contrato que realmente se pretendía implementar.
Las actualizaciones de versión requieren la misma disciplina. Fije Prisma, el paquete de protección y el generador de enrutadores a versiones específicas al mismo tiempo, regénerelos a partir de un checkout limpio y ejecute pruebas de contrato contra el resultado. El código generado sigue formando parte de su superficie de dependencias, incluso si su repositorio no trata cada línea emitida como código escrito a mano por alguien.
La verdadera ventaja en velocidad proviene de la reutilización. Una sola edición del esquema puede actualizar al instante los metadatos de validación, los tipos de cliente y la lógica de enrutamiento. Esto permite concentrar las revisiones en la capa de políticas, que es relativamente más limitada y que realmente no puede derivarse únicamente del modelo.
Deje que un modelo sirva a varios contratos definidos
Un modelo Prisma puede respaldar varias interfaces dirigidas al producto al mismo tiempo.
Por ejemplo, un registro de habitación de hotel podría aparecer en una página de búsqueda pública, en un feed de datos de socios y en una consola interna para el personal. No debería obligarse a estos tres usuarios a compartir un conjunto abultado con todos los campos y operaciones que cualquiera de ellos pudiera necesitar.
Las formas nombradas permiten que una sola operación generada lleve a cabo múltiples contratos distintos al mismo tiempo:
const roomRoutes = {
findMany: {
shape: {
storefront: {
where: {
isPublished: { equals: force(true) },
name: { contains: true },
},
select: { id: true, name: true, nightlyRate: true },
take: { max: 40, default: 20 },
},
backoffice: {
where: {
name: { contains: true },
floor: { equals: true },
},
select: {
id: true,
name: true,
nightlyRate: true,
floor: true,
internalNote: true,
},
take: { max: 200, default: 50 },
},
},
},
}
Cada clave nombrada define un contrato API completo y autónomo. Un usuario público no puede ampliar su proyección para incluir internalNote, ya que ese campo simplemente no existe en la forma pública. El personal puede obtener una proyección mucho más completa sin obligar a los demás clientes a utilizar su propio enrutador personalizado.
Utilice shape cuando solo sea necesario que el contrato a nivel Prisma difiera entre los usuarios. Utilice variants cuando un usuario específico también necesite sus propios ganchos dedicados.
La forma en que se identifica al llamante forma parte del propio límite de seguridad. Un encabezado de solicitud se considera entrada proporcionada por el cliente; es adecuado para distinciones intencionalmente públicas, como una vista compacta frente a una detallada, pero no debe usarse para seleccionar contratos de personal con privilegios.
Para cualquier operación con privilegios, resuelva al llamante mediante resolveVariant utilizando el estado autenticado del lado del servidor. Primero se comparan las claves exactas del llamante y luego las parametrizadas. Una clave default maneja a los llamantes faltantes, en blanco o que no coincidan con nada; por lo tanto, defina solo una cuando esté seguro de que esos tres casos terminarán utilizando esa opción alternativa.
A veces, omitir completamente un contrato vale más que agregar otra verificación de autorización. Si los socios nunca deben poder eliminar salas, simplemente no proporcione ninguna clave de socio a la operación de eliminación generada.
Es útil revisar el enrutamiento de las llamadas como una cuadrícula: las operaciones a lo largo de un eje y el público objetivo a lo largo del otro. Cada celda debe contener una forma con los elementos adecuadamente alineados, o dejarse intencionadamente en blanco.
Mantenga estos contratos con nombres separados incluso cuando coincidan en gran medida en sus campos. Compartir un objeto es razonablemente seguro dentro de un mismo nivel de confianza, pero reutilizar un objeto compartido entre públicos generales y con privilegios corre el riesgo de ampliar silenciosamente ambos extremos en cuanto alguien añada un campo. Un poco de duplicación en los límites de confianza suele valer la pena, ya que facilita enormemente comprender quién obtiene realmente qué datos.
Las claves de llamada parametrizadas le brindan una razón más para confiar en el resolvedor integrado en lugar de reinventar la selección de llamadas dentro de un gancho. El router mantiene el valor bruto de la llamada separado de la clave declarada con la que se comparó, y rechaza por completo los patrones de parámetros ambiguos. Una comparación de cadenas hecha a mano tendría que reproducir la coincidencia exacta, la precedencia de los parámetros, el manejo por defecto y el comportamiento en caso de error antes de poder afirmar que ofrece las mismas garantías.
Use los ganchos para decisiones relacionadas con el ciclo de vida, no para la construcción oculta de consultas
Las rutas generadas no eliminan la necesidad de tomar decisiones a nivel de aplicación. Simplemente proporcionan un lugar predecible para esas decisiones.
Para una solicitud que coincide con una variante específica, el flujo de ejecución pasa por los before-hooks a nivel de operación, luego por los before-hooks a nivel de variante, seguido del propio manejador generado, después por los after-hooks a nivel de variante y, finalmente, por los after-hooks a nivel de operación.
Los hooks de operación son el lugar adecuado para las políticas que se aplican a todos los llamantes de esa operación, independientemente del contrato con el que coincidan. Los hooks de variante corresponden a la lógica específica de una única forma de llamante declarada.
const transferRoutes = {
update: {
before: [authenticateOperator],
variants: {
warehouse: {
before: [authorizeTransferLocation],
shape: warehouseTransferShape,
},
supervisor: {
before: [requireSupervisorApproval],
shape: supervisorTransferShape,
},
},
},
}
Un before-hook puede inspeccionar el identificador exacto que va a utilizar el manejador generado y rechazar la solicitud de inmediato si dicho identificador no pasa una verificación. Lo que nunca debe hacer es autorizar un identificador mientras sustituye silenciosamente otro en la consulta real. Ese tipo de reescritura silenciosa anula el propósito mismo de contar con un manejador inspeccionable.
Las restricciones que nunca cambian deben incluirse en las formas. El filtrado a nivel de inquilino que se aplica al principio de una consulta debe formar parte de las asignaciones de ámbito generadas junto con un contexto confiable. Las diferencias entre los tipos de solicitantes deben incluirse en las variantes. Cada uno de estos elementos tiene un lugar designado, y mezclarlos es la forma en que la lógica termina oculta donde nadie la busca.
Hay casos en los que el servidor realmente necesita crear una consulta que ninguna forma puede expresar. Es entonces cuando un procesador diseñado específicamente resulta útil, sobre todo cuando se necesita una disyunción verdaderamente gestionada por el servidor. Vale la pena recordar que las condiciones forzadas anidadas dentro de combinadores booleanos se convierten en restricciones obligatorias para la consulta, y no en un mecanismo flexible para expresar reglas de autorización arbitrarias. Tratarlos como un motor lógico de uso general es una forma común de terminar con reglas que en realidad no aplican lo que uno cree que aplican.
Los after-hooks se ejecutan después del manejador, pero no constituyen una fase de limpieza en la que se pueda confiar incondicionalmente. Una respuesta que finaliza antes de tiempo o un error generado durante la solicitud pueden impedir que las fases posteriores, incluidos los after-hooks, se ejecuten nunca. Si un recurso debe liberarse bajo cualquier circunstancia, necesita su propio ciclo de vida con un bloque finally explícito fuera de la cadena de ganchos generada, y no dentro de ella.
Las especificaciones también dependen del framework objetivo. Express, Fastify y Hono implementan las firmas de ganchos y el mecanismo de interrupción de forma diferente. El principio general — dónde debe tomarse una decisión determinada — permanece igual en los tres, pero el código de aplicación real debe ajustarse al contrato del framework al que se esté adaptando.
Mantenga el contexto de confianza fuera de los argumentos de Prisma
La identidad del inquilino y el estado del llamante autenticado nunca deben enviarse al servidor como campos controlados por el cliente dentro del cuerpo de la consulta.
En su lugar, aplique un marcador @scope-root al modelo del inquilino, ejecute la generación para crear el mapa de alcance correspondiente y adjunte un resolvedor de contexto al Prisma Client a través de su mecanismo de extensiones:
const prisma = new PrismaClient().$extends(
guard.extension(() => ({
Nursery: requestStore.getStore()?.nurseryId,
}))
)
El valor en sí proviene del estado autenticado y local a la solicitud, no de nada que envíe el cliente. Luego, la extensión lo inyecta en las operaciones de nivel superior compatibles de los modelos que están mapeados como hijos de ese punto de origen de alcance.
Se trata de una característica real y bien definida, no de una garantía general de que toda relación se bloquee automáticamente. La aplicación de los límites de ámbito no abarca las lecturas o escrituras anidadas. El propio modelo de delegado raíz no se filtra mediante su marcador de ámbito. Además, cualquier modelo que carezca de una asignación generada sigue necesitando su propia protección explícita, ya que el contexto de ámbito no lo cubre por defecto.
La velocidad que ofrece la generación sigue siendo valiosa precisamente porque estos límites son visibles y no ocultos. Puedes revisar el mapa de ámbito directamente. Las proyecciones anidadas pueden tener sus propios filtros y límites independientes. Cualquier regla de propiedad inusual que no se ajuste al patrón estándar puede integrarse en el código de la aplicación o gestionarse en la capa de la base de datos.
El estado de la aplicación personalizada —cualquier elemento que vaya más allá del alcance del inquilino— debe encontrarse en el contexto de la solicitud, no en los propios argumentos de Prisma. Incluir información sobre la identidad del llamante o metadatos de autorización en el cuerpo de la solicitud de Prisma hace que el contrato de datos resultante sea mucho más difícil de comprender, y también puede activar validaciones estrictas que esperan una estructura de argumentos limpia.
Trate la exposición de rutas como parte del diseño del producto
Un generador es capaz de crear controladores para un gran número de operaciones de Prisma. Esa capacidad no indica cuáles de esos controladores deberían estar realmente conectados y accesibles.
Las lecturas, las mutaciones de un único registro, las mutaciones masivas, las escrituras de relaciones y las operaciones que devuelven datos merecen todas una revisión por separado, y no una decisión general única. El soporte del proveedor para algunas operaciones masivas que devuelven datos varía, por lo que esto no es solo una cuestión de políticas, sino también de compatibilidad. Cualquier ruta que quede sin un shape y sin variants se comunicará directamente con Prisma sin ninguna verificación de seguridad.
Una configuración bien pensada no es el resultado de activar todo y luego añadir comprobaciones de denegación posteriormente. Comienza con un conjunto pequeño y explícito de elementos, y solo se expande cuando un flujo de trabajo real del producto demuestra la necesidad de otra operación.
La proyección de lectura requiere el mismo nivel de atención que el acceso de escritura. En una lectura protegida, un select o include declarado a nivel de forma funciona tanto como lista blanca como valor predeterminado siempre que la solicitud del cliente omita su propia proyección. La proyección de mutación sigue reglas predeterminadas diferentes, y si nunca se debe permitir que la omisión de una proyección amplíe la respuesta, es necesario tener enforceProjection activado explícitamente.
Las rutas de procesamiento masivo requieren una decisión distinta cada vez. Un método de tipo masivo solo debe considerarse válido en la superficie generada cuando su estructura establezca un vocabulario de filtrado adecuado, y la solicitud recibida siga proporcionando una condición significativa en tiempo de ejecución. Activar deleteMany simplemente porque ya se permite la eliminación de un único registro ignora ese segundo riesgo separado. La utilización de variantes de operaciones masivas conlleva su propia dependencia del proveedor y del soporte de Prisma, por lo que la configuración de las rutas debe reflejar lo que la base de datos desplegada puede ejecutar realmente, y no lo que un plan de desarrollo del producto preferiría que se ejecutara.
La salida generada por OpenAPI puede describir las rutas y la estructura de las solicitudes derivadas de los esquemas. No puede acceder a funciones de gancho arbitrarias, por lo que no puede describir las políticas ocultas dentro de ellas. Si un gancho bloquea transferencias que están fuera del almacén asignado a un operador, esa condición debe documentarse junto a la configuración de la ruta y verificarse con pruebas dirigidas al comportamiento de la aplicación; la documentación generada nunca debe confundirse con prueba de una lógica que no tiene forma de inspeccionar.
Las versiones GET y POST de un endpoint de lectura deben compartir el mismo contrato de consulta. GET depende de parámetros de consulta codificados; POST acepta JSON nativo, lo cual es más práctico para estructuras de argumentos más complejas. Un gancho que solo modifica el cuerpo de la solicitud genera un comportamiento que depende silenciosamente del método de transporte, y esa es precisamente la razón por la cual no se deben establecer restricciones importantes allí.
Adoptar la generación sin renunciar a las revisiones
Una forma práctica de evaluar este tipo de configuración sigue una secuencia breve:
- Generar un enrutador para un modelo de solo lectura.
- Exponer únicamente las operaciones que realmente se necesitan.
- Agregar una forma directa con una proyección explícita y un límite de tamaño de página.
- Verificar los argumentos de Prisma que emite realmente el enrutador.
- Agregar un contexto de ámbito confiable si el modelo está mapeado para arrendamiento.
- Dividir una operación en contratos separados para los llamantes solo cuando los públicos difieran realmente.
- Agregar ganchos únicamente para las decisiones que las formas, el ámbito y las variantes no pueden tomar por sí solos.
- Introducir operaciones de escritura solo después de que la completitud de la creación, el filtrado masivo y la propiedad de las relaciones cuenten con pruebas explícitas que las cubran.
Mantenga las pruebas de contratos real-guard incluso en configuraciones donde las pruebas end-to-end del navegador se ejecutan sin realizar ninguna validación de protección. Las pruebas del navegador son útiles para cubrir el enrutamiento y el comportamiento de la interfaz, pero no pueden demostrar que una versión de producción rechace un campo no permitido cuando en realidad no existe la capa de protección.
La generación cobra sentido al permitir que el equipo se enfoque en las decisiones que realmente importan. Prisma describe los datos; los generadores se encargan del trabajo mecánico repetitivo. Las estructuras definen qué llamadas están permitidas. El código de la aplicación se encarga de proporcionar la confianza, las políticas específicas del producto y las excepciones que no pueden declararse de otra manera honesta.
Lecturas relacionadas
- Construyendo una API GraphQL segura con tipos con Prisma y Nexus en Node.js — Sigue una guía de siete pasos para crear una API GraphQL en Node.js que unifique el modelo de datos de Prisma con los tipos y resolvers generados por Nexus.
- Zod vs express-validator: Dos enfoques para la validación en Express — Compara la validación de solicitudes basada en esquemas con Zod frente a los middleware basados en cadenas de express-validator, abordando la configuración, el formato de los errores y las trampas más comunes.