CRUD completo
Piezas que se combinan
Sección titulada «Piezas que se combinan»REST/CRUD (convenciones de rutas y status codes) + Prisma (persistencia) + respuestas de error (formato consistente).
Router completo
Sección titulada «Router completo»import { Router } from "express"import { z } from "zod"import { AppError } from "../errors/AppError"import { prisma } from "../lib/prisma"import { asyncHandler } from "../utils/asyncHandler"
export const postsRouter = Router()
const crearPostSchema = z.object({ title: z.string().min(1), content: z.string().optional(), published: z.boolean().default(false)})
// GET /posts — listarpostsRouter.get( "/", asyncHandler(async (req, res) => { const posts = await prisma.post.findMany() res.json(posts) }))
// GET /posts/:id — unopostsRouter.get( "/:id", asyncHandler(async (req, res) => { const post = await prisma.post.findUnique({ where: { id: req.params.id } }) if (!post) throw new AppError(404, "POST_NO_ENCONTRADO", "Post no encontrado") res.json(post) }))
// POST /posts — crearpostsRouter.post( "/", asyncHandler(async (req, res) => { const datos = crearPostSchema.parse(req.body) // lanza si es inválido, lo atrapa asyncHandler const post = await prisma.post.create({ data: datos }) res.status(201).json(post) }))
// PATCH /posts/:id — actualizar parcialpostsRouter.patch( "/:id", asyncHandler(async (req, res) => { const datos = crearPostSchema.partial().parse(req.body) // todos los campos opcionales aquí const post = await prisma.post.update({ where: { id: req.params.id }, data: datos }) res.json(post) }))
// DELETE /posts/:id — eliminarpostsRouter.delete( "/:id", asyncHandler(async (req, res) => { await prisma.post.delete({ where: { id: req.params.id } }) res.status(204).end() }))import { postsRouter } from "./routes/posts.routes"
app.use("/posts", postsRouter)app.use(errorHandler) // al final, atrapa el ZodError y cualquier AppError lanzado arribaManejar el error de Zod en el error handler
Sección titulada «Manejar el error de Zod en el error handler»import { ZodError } from "zod"import { AppError } from "../errors/AppError"
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { if (err instanceof ZodError) { return res.status(400).json({ error: { code: "VALIDATION_ERROR", fields: err.flatten().fieldErrors } }) }
if (err instanceof AppError) { return res .status(err.status) .json({ error: { code: err.code, message: err.message } }) }
console.error(err) res .status(500) .json({ error: { code: "INTERNAL_ERROR", message: "Error interno" } })}Consideraciones
Sección titulada «Consideraciones»crearPostSchema.partial()en elPATCHreusa el mismo schema delPOST, pero con todos los campos opcionales — evita mantener dos schemas casi idénticos.- Este ejemplo no incluye auth ni ownership (cualquiera puede editar cualquier post) — combinar con Middleware de autenticación y el nivel de ownership de Roles y permisos para una versión protegida real (ver también API protegida).