API protegida en Astro
El mismo checklist que en Express, sin middleware por ruta
Sección titulada «El mismo checklist que en Express, sin middleware por ruta»API protegida (Express) documenta el checklist completo: auth → rol → rate limit → validación. En Astro, sin cadena de middlewares específica por endpoint, esas capas se resuelven dentro del handler mismo (además del middleware global de auth que puebla locals.user).
import type { APIRoute } from "astro"import { z } from "zod"import { prisma } from "../../../../lib/prisma"import { checarRateLimit } from "../../../../lib/rate-limit"
const actualizarUsuarioSchema = z.object({ rol: z.enum(["user", "moderador", "admin"]).optional(), activo: z.boolean().optional()})
export const PATCH: APIRoute = async ({ params, request, locals, clientAddress}) => { // 1. ¿quién eres? (ya resuelto por el middleware global, ver Backend en Astro) if (!locals.user) { return new Response(JSON.stringify({ error: { code: "NO_AUTENTICADO" } }), { status: 401 }) }
// 2. ¿puedes hacer esto? if (locals.user.rol !== "admin") { return new Response(JSON.stringify({ error: { code: "SIN_PERMISO" } }), { status: 403 }) }
// 3. ¿estás abusando de este endpoint? const dentroDelLimite = await checarRateLimit(clientAddress, { max: 30, windowMs: 15 * 60 * 1000 }) if (!dentroDelLimite) { return new Response(JSON.stringify({ error: { code: "RATE_LIMIT" } }), { status: 429 }) }
// 4. ¿el body tiene forma válida? const body = await request.json() const resultado = actualizarUsuarioSchema.safeParse(body) if (!resultado.success) { return new Response( JSON.stringify({ error: { code: "VALIDATION_ERROR", fields: resultado.error.flatten().fieldErrors } }), { status: 400 } ) }
const usuario = await prisma.user.update({ where: { id: params.id }, data: resultado.data }) return new Response(JSON.stringify(usuario), { headers: { "Content-Type": "application/json" } })}Un rate limiter simple (en memoria, para un solo proceso)
Sección titulada «Un rate limiter simple (en memoria, para un solo proceso)»const intentos = new Map<string, { count: number; resetAt: number }>()
export async function checarRateLimit( ip: string, { max, windowMs }: { max: number; windowMs: number }): Promise<boolean> { const ahora = Date.now() const registro = intentos.get(ip)
if (!registro || ahora > registro.resetAt) { intentos.set(ip, { count: 1, resetAt: ahora + windowMs }) return true }
if (registro.count >= max) return false
registro.count++ return true}Resumen
Sección titulada «Resumen»| Capa | Dónde vive en Astro |
|---|---|
| Auth | Middleware global (locals.user), chequeado al inicio del handler |
| Rol/permisos | if dentro del handler (o un helper reusable como requierePermiso en Express) |
| Rate limiting | Función propia, o un servicio externo (Upstash, Cloudflare) en producción real |
| Validación | Zod, safeParse dentro del handler |
Consideraciones
Sección titulada «Consideraciones»- El rate limiter en memoria del ejemplo no se comparte entre instancias ni sobrevive un reinicio. En producción con más de una instancia, usa un store compartido como Redis o un servicio compatible.
- El razonamiento de “por qué ese orden” (barato primero, validación al final) es el mismo que en API protegida (Express) — no cambia por el framework, solo la sintaxis de dónde se escribe cada chequeo.