src/app/api/**/route.ts siguen un patrón consistente, en
este orden:
1. requireAdmin() en todo /api/admin/*
Todo endpoint bajo /api/admin/* empieza llamando a requireAdmin() de
@/lib/require-admin:
requireAdmin() devuelve { error, session }. Si error no es null, el
handler lo retorna tal cual — ya es un 401 (sin sesión) o un 403 (sesión
sin rol ADMIN).
2. Validación con Zod safeParse
Los searchParams o el body se validan con safeParse. Si falla, la
respuesta es 400 con esta forma:
issues es la salida de parsed.error.flatten().
Si el endpoint recibe un body JSON, request.json() va en su propio
try/catch, antes del try general. Un body mal formado es un error del
cliente: responde 400 con { "error": "JSON invalido" } en vez de caer en
el 500 del paso 4:
3. Listados paginados por cursor
Los listados paginan contake: limit + 1 y, si hay cursor, con
cursor: { id: cursor } más skip: 1. La fila extra sirve para saber si hay
otra página sin hacer un segundo count. La respuesta tiene siempre
nextCursor y hasMore; el nombre de la clave con el array cambia según el
recurso (users, camellos…):
id de la última fila de la página anterior — también en el
listado de usuarios, donde antes era un cursor compuesto codificado en
base64url. Ese cursor especial se retiró cuando el listado de usuarios pasó
a poder ordenarse por columna: con orderBy variable, un id simple es
suficiente y funciona igual que en camellos.
4. try/catch envolviendo todo
Todo el handler queda envuelto en try/catch. El catch registra el error
con un prefijo que identifica el endpoint y responde 500:
catch sin console.error deja los 500 de producción sin rastro en los
logs; no lo omitas aunque el handler sea corto.
Cuando el handler comprueba unicidad con un findUnique previo (por ejemplo,
el email en POST /api/register, POST /api/admin/users y
PATCH /api/admin/users/:id), dos peticiones simultáneas pueden pasar la
comprobación a la vez y la segunda choca con el índice único de la BD. El
catch traduce ese P2002 de Prisma al mismo 409 que da la comprobación,
antes de caer al 500:
/api/admin/categories/:id,
/api/admin/services/:id) traducen así los códigos de Prisma:
Ojo con
Restrict: Job → Service lo usa, así que un servicio con trabajos
(incluso terminados) no se puede borrar. DELETE /api/admin/services/:id
cuenta camellos y jobs antes de borrar y responde 409 con el motivo;
el P2003 del catch solo cubre la carrera.
Si el create escribe más de un campo único, hay que distinguir cuál falló.
Con el driver adapter de MariaDB, el índice no llega en meta.target: ver
violatesUniqueConstraint en src/app/api/register/camello/route.ts.
Idioma de los mensajes
Los mensajes de error de la API van en español sin tildes —"Parametros invalidos", "No autenticado", "Sin permisos de administrador" — porque son valores literales que el cliente puede llegar a
comparar. La UI que los muestra sí usa tildes.