Tipado de Manejadores de Rutas
Receta
Tipifica manejadores de rutas de App Router de Next.js con NextRequest, NextResponse, y parámetros de ruta tipificados. Crea endpoints de API seguros de tipos con contratos de request/response apropiados.
Ejemplo Funcional
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
type User = {
id: string;
name: string;
email: string;
};
type CreateUserBody = {
name: string;
email: string;
};
type ApiErrorResponse = {
error: string;
};
// Manejador GET
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const role = searchParams.get("role");
const users: User[] = await fetchUsersFromDb(role);
return NextResponse.json(users);
}
// Manejador POST con body tipificado
export async function POST(request: NextRequest) {
const body: unknown = await request.json();
if (!isValidCreateUserBody(body)) {
return NextResponse.json(
{ error: "Invalid request body" } satisfies ApiErrorResponse,
{ status: 400 }
);
}
const newUser: User = {
id: crypto.randomUUID(),
name: body.name,
email: body.email,
};
// ... guardar en base de datos
return NextResponse.json(newUser, { status: 201 });
}
// Type guard para el body de la request
function isValidCreateUserBody(body: unknown): body is CreateUserBody {
return (
typeof body === "object" &&
body !== null &&
"name" in body &&
"email" in body &&
typeof (body as CreateUserBody).name === "string" &&
typeof (body as CreateUserBody).email === "string"
);
}// app/api/users/[id]/route.ts - Parámetros de ruta dinámicos
import { NextRequest, NextResponse } from "next/server";
type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
const user = await findUserById(id);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
await deleteUserById(id);
return new NextResponse(null, { status: 204 });
}Análisis Profundo
Cómo Funciona
- Los manejadores de rutas exportan funciones nombradas que coinciden con métodos HTTP:
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS. NextRequestextiende la Web APIRequestcon métodos de conveniencia comonextUrl.searchParamspara acceso a parámetros de query.NextResponse.json(data, init?)crea una respuesta JSON con headersContent-Typeapropiados. El parámetrodataacepta cualquier valor serializable.- En Next.js 15, los
paramsde ruta están envueltos en unaPromisey deben ser awaited. Esto se alinea con el patrón de params async usado en componentes de página. - Los manejadores de rutas son código solo de servidor. Pueden acceder de forma segura a bases de datos, variables de entorno, y secretos del lado del servidor.
Variaciones
Validación con Zod en manejadores de rutas:
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = CreateUserSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validation failed", details: parsed.error.flatten() },
{ status: 400 }
);
}
const { name, email } = parsed.data; // Completamente tipificado
// ... crear usuario
}Configurando headers y cookies:
export async function GET(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const response = NextResponse.json({ data: "protected" });
response.headers.set("X-Custom-Header", "value");
return response;
}Respuesta con streaming:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const chunk of ["Hello", " ", "World"]) {
controller.enqueue(encoder.encode(chunk));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new NextResponse(stream, {
headers: { "Content-Type": "text/plain" },
});
}Notas de TypeScript
request.json()devuelvePromise<any>. Siempre valida o convierte el resultado antes de usarlo.NextResponse.json()acepta cualquier valor. Usasatisfiespara verificar la forma:NextResponse.json({ error: "msg" } satisfies ApiErrorResponse).- Los parámetros de ruta siempre son strings. Incluso parámetros que se ven como números como
/api/users/123tienenidcomostring. Parseálos explícitamente:parseInt(id, 10).
Errores Comunes
- En Next.js 15,
paramses unaPromise. Olvidar hacerawaitte da un objetoPromiseen lugar de los parámetros. Este es un problema común en la migración desde Next.js 14. request.json()lanza un error si el body no es JSON válido. Envuélvelo en un try/catch para robustez.- Los manejadores de rutas no tienen acceso al árbol de componentes React. No pueden usar hooks o context.
- Retornar
new Response()en lugar deNextResponsefunciona pero pierde características específicas de Next.js como los helperscookies()yheaders().
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Manejadores de Rutas (App Router) | APIs estándar web, soporte de streaming | Validación manual requerida |
| Server Actions | Integración más estrecha con formularios React | No para consumidores de API de terceros |
| API Routes (Pages Router) | Patrón req/res familiar | Legacy, NextApiRequest es específico de Node |
| tRPC | Seguridad de tipos end-to-end | Dependencia adicional |
| API Externa (Express, Fastify) | Control completo, despliegue independiente | Servicio separado para mantener |
FAQs
¿Qué exportaciones de métodos HTTP soporta un archivo manejador de rutas?
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS- Exporta cada una como una función async nombrada en el archivo
route.ts.
¿Cómo accedo a los parámetros de query en un manejador de rutas?
export async function GET(request: NextRequest) {
const role = request.nextUrl.searchParams.get("role");
// role es string | null
}¿Por qué request.json() está tipificado como Promise<any> y qué debo hacer al respecto?
- La Web API
Request.json()no tiene forma de conocer la forma de tu payload en tiempo de compilación. - Siempre valida o estrecha el resultado antes de usarlo (type guard, Zod, o conversión explícita después de verificaciones).
¿Cómo tipfico parámetros de ruta dinámicos en Next.js 15?
type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(request: NextRequest, context: RouteContext) {
const { id } = await context.params;
}- Los parámetros están envueltos en una
Promisey deben ser awaited.
Error Común: ¿Qué pasa si olvido hacer await a params en Next.js 15?
- Obtienes un objeto
Promiseen lugar de los parámetros actuales. - Este es un problema común en la migración desde Next.js 14 donde los parámetros eran síncronos.
¿Para qué se usa la palabra clave satisfies en manejadores de rutas?
- Valida que un valor coincida con un tipo sin ampliarlo:
return NextResponse.json({ error: "msg" } satisfies ApiErrorResponse);- La forma se verifica en tiempo de compilación, pero el valor en tiempo de ejecución no cambia.
¿Cómo valido request bodies con Zod en un manejador de rutas?
- Parsea el body con
safeParsey verificaparsed.success:
const parsed = CreateUserSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const { name, email } = parsed.data; // Completamente tipificado¿Los parámetros de ruta son siempre strings incluso si se ven como números?
- Sí. Un parámetro como
/api/users/123te daidcomo"123"(string). - Parsea explícitamente con
parseInt(id, 10)oNumber(id)si necesitas un número.
Error Común: ¿Qué pasa si el body de la request no es JSON válido?
request.json()lanza un error en tiempo de ejecución.- Siempre envuélvelo en un try/catch para robustez en manejadores de rutas de producción.
¿Pueden los manejadores de rutas usar React hooks o acceder al árbol de componentes?
- No. Los manejadores de rutas son código solo de servidor que se ejecuta fuera del árbol de componentes React.
- No pueden usar hooks, context, o ninguna API React del lado del cliente.
¿Cuál es la diferencia entre retornar Response y NextResponse?
new Response()funciona pero carece de helpers específicos de Next.js comocookies()yheaders().NextResponseextiendeResponsecon métodos de conveniencia. Prefierelo para características de Next.js.
¿Cómo escribo una type guard para un body de request?
function isValidBody(body: unknown): body is CreateUserBody {
return (
typeof body === "object" &&
body !== null &&
"name" in body &&
typeof (body as CreateUserBody).name === "string"
);
}