Saltar al contenido principal
Volver al blog

API Routes en Next.js: diseño, validación y seguridad

Ray MartínRay Martín
11 min de lectura
API Routes en Next.js: diseño, validación y seguridad

Route Handlers en el App Router de Next.js

El App Router de Next.js introdujo un nuevo paradigma para construir endpoints de API: los Route Handlers. A diferencia del antiguo directorio pages/api, los Route Handlers utilizan las APIs estandar de la Web Request y Response, haciendolos mas portables, testeables y alineados con los estandares web modernos. Conviven junto a tus paginas en el directorio app/, siguiendo las mismas convenciones de enrutamiento basado en archivos.

Un Route Handler se define exportando funciones asincronas nombradas segun los metodos HTTP desde un archivo route.ts. Cada funcion recibe un objeto Request estandar y devuelve un Response o NextResponse.

typescript
// app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ message: "Hola desde la API" });
}

export async function POST(request: Request) {
  const body = await request.json();
  return NextResponse.json(
    { received: body },
    { status: 201 }
  );
}

export async function PUT(request: Request) {
  const body = await request.json();
  return NextResponse.json({ updated: body });
}

export async function DELETE(request: Request) {
  return NextResponse.json(
    { deleted: true },
    { status: 200 }
  );
}

Solo los metodos HTTP que exportes estaran disponibles. Si un cliente envia una solicitud con un metodo no soportado, Next.js devuelve automaticamente una respuesta 405 Method Not Allowed.

Parseo de solicitudes: Params, SearchParams y Body

Entender como extraer datos de las solicitudes entrantes es fundamental. Los Route Handlers proporcionan tres fuentes principales de datos: parametros de ruta, parametros de busqueda de URL y el cuerpo de la solicitud.

Parametros de ruta

Los segmentos de ruta dinamicos se definen usando corchetes en el nombre de la carpeta y se pasan como segundo argumento al handler:

typescript
// app/api/projects/[id]/route.ts
import { NextResponse } from "next/server";

interface RouteParams {
  params: Promise<{ id: string }>;
}

export async function GET(request: Request, { params }: RouteParams) {
  const { id } = await params;

  // Validar el formato del ID
  if (!id || typeof id !== "string") {
    return NextResponse.json(
      { error: "ID de proyecto invalido" },
      { status: 400 }
    );
  }

  const project = await getProjectById(id);

  if (!project) {
    return NextResponse.json(
      { error: "Proyecto no encontrado" },
      { status: 404 }
    );
  }

  return NextResponse.json(project);
}

Parametros de busqueda

Los parametros de consulta de la URL se extraen de la URL de la solicitud usando la API estandar URL:

typescript
// app/api/projects/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const page = parseInt(searchParams.get("page") || "1", 10);
  const limit = parseInt(searchParams.get("limit") || "10", 10);
  const category = searchParams.get("category");
  const sortBy = searchParams.get("sortBy") || "createdAt";
  const order = searchParams.get("order") || "desc";

  const projects = await getProjects({
    page,
    limit,
    category: category || undefined,
    sortBy,
    order: order as "asc" | "desc",
  });

  return NextResponse.json(projects);
}

Cuerpo de la solicitud

El cuerpo de la solicitud puede parsearse como JSON, FormData, texto o un blob binario dependiendo del tipo de contenido:

typescript
// Cuerpo JSON
export async function POST(request: Request) {
  const body = await request.json();
  // body tiene tipo 'any' — validar con Zod a continuacion
}

// Cuerpo FormData
export async function POST(request: Request) {
  const formData = await request.formData();
  const name = formData.get("name") as string;
  const file = formData.get("file") as File;
}

// Cuerpo de texto
export async function POST(request: Request) {
  const text = await request.text();
}

// Cuerpo binario
export async function POST(request: Request) {
  const buffer = await request.arrayBuffer();
}

Validacion de entrada con Zod

Nunca confies en la entrada del usuario. Cada ruta de API debe validar su entrada antes de procesarla. Zod es el estandar para la validacion de esquemas con TypeScript — valida en tiempo de ejecucion e infiere tipos TypeScript a partir de la definicion del esquema.

bash
npm install zod
typescript
// lib/validations/contact.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z
    .string()
    .min(2, "El nombre debe tener al menos 2 caracteres")
    .max(100, "El nombre debe tener menos de 100 caracteres")
    .trim(),
  email: z
    .string()
    .email("Direccion de email invalida")
    .toLowerCase(),
  subject: z
    .string()
    .min(5, "El asunto debe tener al menos 5 caracteres")
    .max(200, "El asunto debe tener menos de 200 caracteres")
    .trim(),
  message: z
    .string()
    .min(10, "El mensaje debe tener al menos 10 caracteres")
    .max(5000, "El mensaje debe tener menos de 5000 caracteres")
    .trim(),
  locale: z.enum(["en", "es"]).default("es"),
});

export type ContactInput = z.infer<typeof contactSchema>;
typescript
// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { contactSchema } from "@/lib/validations/contact";
import { ZodError } from "zod";

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const validatedData = contactSchema.parse(body);

    // validatedData tiene el tipo completo ContactInput
    await sendEmail({
      to: process.env.MAILJET_TO_EMAIL!,
      subject: validatedData.subject,
      name: validatedData.name,
      email: validatedData.email,
      message: validatedData.message,
    });

    return NextResponse.json(
      { success: true, message: "Email enviado correctamente" },
      { status: 200 }
    );
  } catch (error) {
    if (error instanceof ZodError) {
      return NextResponse.json(
        {
          error: "Validacion fallida",
          details: error.errors.map((err) => ({
            field: err.path.join("."),
            message: err.message,
          })),
        },
        { status: 400 }
      );
    }

    console.error("Error en API de contacto:", error);
    return NextResponse.json(
      { error: "Error interno del servidor" },
      { status: 500 }
    );
  }
}

Para validaciones mas complejas, Zod soporta refinamientos, transformaciones y uniones discriminadas:

typescript
// Patrones avanzados de Zod
const projectSchema = z.object({
  title: z.string().min(3).max(100),
  description: z.string().min(10).max(2000),
  url: z.string().url().optional(),
  tags: z.array(z.string()).min(1).max(10),
  budget: z.number().positive().max(1000000),
  startDate: z.string().datetime(),
  endDate: z.string().datetime().optional(),
}).refine(
  (data) => {
    if (data.endDate) {
      return new Date(data.endDate) > new Date(data.startDate);
    }
    return true;
  },
  {
    message: "La fecha de fin debe ser posterior a la fecha de inicio",
    path: ["endDate"],
  }
);

Patrones de manejo de errores

El manejo consistente de errores es esencial para una API confiable. Define un formato estandar de respuesta de error y usalo en todas tus rutas. Esto facilita que el codigo frontend maneje los errores de forma predecible.

typescript
// lib/api/errors.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public code: string,
    public details?: Record<string, unknown>
  ) {
    super(message);
    this.name = "ApiError";
  }
}

export class NotFoundError extends ApiError {
  constructor(resource: string) {
    super(
      `${resource} no encontrado`,
      404,
      "NOT_FOUND"
    );
  }
}

export class ValidationError extends ApiError {
  constructor(message: string, details?: Record<string, unknown>) {
    super(message, 400, "VALIDATION_ERROR", details);
  }
}

export class UnauthorizedError extends ApiError {
  constructor(message = "Autenticacion requerida") {
    super(message, 401, "UNAUTHORIZED");
  }
}

export class ForbiddenError extends ApiError {
  constructor(message = "Permisos insuficientes") {
    super(message, 403, "FORBIDDEN");
  }
}

export class RateLimitError extends ApiError {
  constructor(retryAfter: number) {
    super(
      "Demasiadas solicitudes",
      429,
      "RATE_LIMIT_EXCEEDED",
      { retryAfter }
    );
  }
}
typescript
// lib/api/handler.ts
import { NextResponse } from "next/server";
import { ZodError } from "zod";
import { ApiError } from "./errors";

interface ApiErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
}

export function handleApiError(error: unknown): NextResponse<ApiErrorResponse> {
  // Errores API conocidos
  if (error instanceof ApiError) {
    return NextResponse.json(
      {
        error: {
          code: error.code,
          message: error.message,
          details: error.details,
        },
      },
      { status: error.statusCode }
    );
  }

  // Errores de validacion Zod
  if (error instanceof ZodError) {
    return NextResponse.json(
      {
        error: {
          code: "VALIDATION_ERROR",
          message: "Datos de entrada invalidos",
          details: {
            fields: error.errors.map((e) => ({
              path: e.path.join("."),
              message: e.message,
            })),
          },
        },
      },
      { status: 400 }
    );
  }

  // Errores desconocidos
  console.error("Error API no manejado:", error);
  return NextResponse.json(
    {
      error: {
        code: "INTERNAL_ERROR",
        message: "Ha ocurrido un error inesperado",
      },
    },
    { status: 500 }
  );
}

Ahora tus route handlers quedan limpios y enfocados en la logica de negocio:

typescript
// app/api/projects/[id]/route.ts
import { NextResponse } from "next/server";
import { handleApiError } from "@/lib/api/handler";
import { NotFoundError } from "@/lib/api/errors";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const { id } = await params;
    const project = await getProjectById(id);

    if (!project) {
      throw new NotFoundError("Proyecto");
    }

    return NextResponse.json(project);
  } catch (error) {
    return handleApiError(error);
  }
}

Middleware de autenticacion con funciones de orden superior

Proteger las rutas de API con autenticacion es una preocupacion transversal que no deberia duplicarse en cada route handler. Usa un patron de funcion de orden superior para envolver las rutas con logica de autenticacion:

typescript
// lib/api/withAuth.ts
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { verifyToken } from "@/lib/auth";
import { UnauthorizedError, ForbiddenError } from "./errors";
import { handleApiError } from "./handler";

interface AuthenticatedUser {
  id: string;
  email: string;
  role: "admin" | "user";
}

type AuthenticatedHandler = (
  request: Request,
  context: {
    params: Promise<Record<string, string>>;
    user: AuthenticatedUser;
  }
) => Promise<NextResponse>;

export function withAuth(
  handler: AuthenticatedHandler,
  options?: { roles?: string[] }
) {
  return async (
    request: Request,
    context: { params: Promise<Record<string, string>> }
  ) => {
    try {
      const headersList = await headers();
      const authorization = headersList.get("authorization");

      if (!authorization || !authorization.startsWith("Bearer ")) {
        throw new UnauthorizedError("Cabecera de autorizacion ausente o invalida");
      }

      const token = authorization.slice(7);
      const user = await verifyToken(token);

      if (!user) {
        throw new UnauthorizedError("Token invalido o expirado");
      }

      // Verificar acceso basado en roles
      if (options?.roles && !options.roles.includes(user.role)) {
        throw new ForbiddenError("Permisos insuficientes para este recurso");
      }

      return handler(request, { ...context, user });
    } catch (error) {
      return handleApiError(error);
    }
  };
}
typescript
// app/api/admin/users/route.ts
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/api/withAuth";

export const GET = withAuth(
  async (request, { user }) => {
    // user tiene tipo y esta garantizado como autenticado
    const users = await getAllUsers();
    return NextResponse.json(users);
  },
  { roles: ["admin"] } // Solo los admins pueden acceder a este endpoint
);

export const POST = withAuth(
  async (request, { user }) => {
    const body = await request.json();
    const newUser = await createUser(body);
    return NextResponse.json(newUser, { status: 201 });
  },
  { roles: ["admin"] }
);

Configuracion de CORS

Si tu API es consumida por clientes externos o dominios diferentes, necesitas configurar Cross-Origin Resource Sharing (CORS). Next.js no habilita CORS por defecto para las rutas de API.

typescript
// lib/api/cors.ts
import { NextResponse } from "next/server";

const ALLOWED_ORIGINS = [
  "https://raymartin.es",
  "https://www.raymartin.es",
  process.env.NODE_ENV === "development" && "http://localhost:3000",
].filter(Boolean) as string[];

export function corsHeaders(origin: string | null) {
  const headers = new Headers();

  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    headers.set("Access-Control-Allow-Origin", origin);
  }

  headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
  headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
  headers.set("Access-Control-Max-Age", "86400");

  return headers;
}

export function withCors(handler: (request: Request) => Promise<NextResponse>) {
  return async (request: Request) => {
    const origin = request.headers.get("origin");

    // Manejar solicitudes preflight
    if (request.method === "OPTIONS") {
      return new NextResponse(null, {
        status: 204,
        headers: corsHeaders(origin),
      });
    }

    const response = await handler(request);

    // Agregar cabeceras CORS a la respuesta
    const headers = corsHeaders(origin);
    headers.forEach((value, key) => {
      response.headers.set(key, value);
    });

    return response;
  };
}
typescript
// app/api/public/data/route.ts
import { NextResponse } from "next/server";
import { withCors } from "@/lib/api/cors";

export const GET = withCors(async (request: Request) => {
  const data = await getPublicData();
  return NextResponse.json(data);
});

// Manejar OPTIONS preflight
export const OPTIONS = withCors(async () => {
  return new NextResponse(null, { status: 204 });
});

Estrategias de rate limiting

El rate limiting protege tu API contra abusos y garantiza un uso justo. Para Next.js desplegado en Vercel, puedes implementar rate limiting con un almacen en memoria para desarrollo y un almacen basado en Redis para produccion.

typescript
// lib/api/rateLimit.ts
interface RateLimitEntry {
  count: number;
  resetAt: number;
}

const rateLimitMap = new Map<string, RateLimitEntry>();

interface RateLimitOptions {
  windowMs: number;    // Ventana de tiempo en milisegundos
  maxRequests: number; // Maximo de solicitudes por ventana
}

export function rateLimit(
  identifier: string,
  options: RateLimitOptions = { windowMs: 60_000, maxRequests: 10 }
): { success: boolean; remaining: number; resetAt: number } {
  const now = Date.now();
  const entry = rateLimitMap.get(identifier);

  // Limpiar entradas expiradas
  if (entry && now > entry.resetAt) {
    rateLimitMap.delete(identifier);
  }

  const current = rateLimitMap.get(identifier);

  if (!current) {
    const resetAt = now + options.windowMs;
    rateLimitMap.set(identifier, { count: 1, resetAt });
    return { success: true, remaining: options.maxRequests - 1, resetAt };
  }

  if (current.count >= options.maxRequests) {
    return { success: false, remaining: 0, resetAt: current.resetAt };
  }

  current.count += 1;
  return {
    success: true,
    remaining: options.maxRequests - current.count,
    resetAt: current.resetAt,
  };
}
typescript
// app/api/contact/route.ts
import { NextResponse } from "next/server";
import { rateLimit } from "@/lib/api/rateLimit";

export async function POST(request: Request) {
  // Rate limit por direccion IP
  const forwarded = request.headers.get("x-forwarded-for");
  const ip = forwarded?.split(",")[0]?.trim() || "unknown";

  const { success, remaining, resetAt } = rateLimit(ip, {
    windowMs: 60_000,   // Ventana de 1 minuto
    maxRequests: 5,      // 5 solicitudes por minuto
  });

  if (!success) {
    return NextResponse.json(
      { error: "Demasiadas solicitudes. Por favor intentalo mas tarde." },
      {
        status: 429,
        headers: {
          "Retry-After": String(Math.ceil((resetAt - Date.now()) / 1000)),
          "X-RateLimit-Remaining": "0",
        },
      }
    );
  }

  // Procesar la solicitud normalmente...
  const body = await request.json();

  const response = NextResponse.json({ success: true });
  response.headers.set("X-RateLimit-Remaining", String(remaining));

  return response;
}

Patrones de paginacion

Hay dos estrategias comunes de paginacion: basada en offset y basada en cursor. Cada una tiene ventajas y desventajas que la hacen adecuada para diferentes casos de uso.

Paginacion basada en offset

La paginacion por offset usa un numero de pagina y un tamano de pagina. Es simple de implementar y permite saltar a cualquier pagina, pero puede ser lenta en conjuntos de datos grandes y sufre problemas de consistencia cuando se insertan o eliminan datos entre paginas.

typescript
// app/api/projects/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";

const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const { page, limit } = paginationSchema.parse({
    page: searchParams.get("page"),
    limit: searchParams.get("limit"),
  });

  const offset = (page - 1) * limit;
  const [projects, total] = await Promise.all([
    getProjects({ offset, limit }),
    countProjects(),
  ]);

  const totalPages = Math.ceil(total / limit);

  return NextResponse.json({
    data: projects,
    pagination: {
      page,
      limit,
      total,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1,
    },
  });
}

Paginacion basada en cursor

La paginacion por cursor usa un cursor opaco (tipicamente un ID o timestamp codificado) para obtener el siguiente conjunto de resultados. Es mas eficiente en conjuntos de datos grandes y maneja inserciones y eliminaciones de forma elegante, pero no soporta saltar a paginas arbitrarias.

typescript
// app/api/feed/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";

const cursorSchema = z.object({
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(50).default(20),
});

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const { cursor, limit } = cursorSchema.parse({
    cursor: searchParams.get("cursor"),
    limit: searchParams.get("limit"),
  });

  const decodedCursor = cursor
    ? JSON.parse(Buffer.from(cursor, "base64url").toString())
    : null;

  // Obtener un elemento extra para determinar si hay mas resultados
  const items = await getFeedItems({
    cursor: decodedCursor,
    limit: limit + 1,
  });

  const hasMore = items.length > limit;
  const data = hasMore ? items.slice(0, limit) : items;

  const nextCursor = hasMore
    ? Buffer.from(
        JSON.stringify({ id: data[data.length - 1].id })
      ).toString("base64url")
    : null;

  return NextResponse.json({
    data,
    pagination: {
      nextCursor,
      hasMore,
    },
  });
}

Subida de archivos con FormData

La gestion de subida de archivos en Route Handlers usa la API estandar FormData. La clave es validar los tipos y tamanos de archivo antes de procesarlos.

typescript
// app/api/upload/route.ts
import { NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import path from "path";

const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

export async function POST(request: Request) {
  try {
    const formData = await request.formData();
    const file = formData.get("file") as File | null;

    if (!file) {
      return NextResponse.json(
        { error: "No se proporciono ningun archivo" },
        { status: 400 }
      );
    }

    // Validar tipo de archivo
    if (!ALLOWED_TYPES.includes(file.type)) {
      return NextResponse.json(
        { error: `El tipo de archivo ${file.type} no esta permitido. Tipos permitidos: ${ALLOWED_TYPES.join(", ")}` },
        { status: 400 }
      );
    }

    // Validar tamano del archivo
    if (file.size > MAX_SIZE) {
      return NextResponse.json(
        { error: `El tamano del archivo excede el limite de ${MAX_SIZE / 1024 / 1024}MB` },
        { status: 400 }
      );
    }

    // Generar un nombre de archivo unico
    const timestamp = Date.now();
    const extension = file.name.split(".").pop();
    const filename = `${timestamp}-${Math.random().toString(36).slice(2)}.${extension}`;

    // Convertir el archivo a un buffer
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);

    // Guardar en el directorio de uploads
    const uploadsDir = path.join(process.cwd(), "public", "uploads");
    await mkdir(uploadsDir, { recursive: true });
    await writeFile(path.join(uploadsDir, filename), buffer);

    return NextResponse.json(
      {
        success: true,
        file: {
          name: filename,
          url: `/uploads/${filename}`,
          size: file.size,
          type: file.type,
        },
      },
      { status: 201 }
    );
  } catch (error) {
    console.error("Error de subida:", error);
    return NextResponse.json(
      { error: "Fallo al subir el archivo" },
      { status: 500 }
    );
  }
}

En el lado del cliente, envia archivos usando FormData:

typescript
"use client";

async function uploadFile(file: File) {
  const formData = new FormData();
  formData.append("file", file);

  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData,
    // NO establecer la cabecera Content-Type — el navegador la establece con el boundary
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error);
  }

  return response.json();
}

Testing de rutas de API

Los Route Handlers son funciones que reciben un Request y devuelven un Response, lo que hace que sean directos de testear sin levantar un servidor. Puedes testearlos directamente construyendo objetos Request y verificando el Response devuelto.

typescript
// __tests__/api/contact.test.ts
import { POST } from "@/app/api/contact/route";

describe("POST /api/contact", () => {
  it("devuelve 200 para entrada valida", async () => {
    const request = new Request("http://localhost:3000/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: "Juan Perez",
        email: "juan@ejemplo.com",
        subject: "Hola desde el test",
        message: "Este es un mensaje de prueba con suficientes caracteres.",
      }),
    });

    const response = await POST(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data.success).toBe(true);
  });

  it("devuelve 400 para email invalido", async () => {
    const request = new Request("http://localhost:3000/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: "Juan Perez",
        email: "no-es-un-email",
        subject: "Asunto de prueba",
        message: "Este es un mensaje de prueba con suficientes caracteres.",
      }),
    });

    const response = await POST(request);
    const data = await response.json();

    expect(response.status).toBe(400);
    expect(data.error).toBeDefined();
  });

  it("devuelve 400 para campos requeridos faltantes", async () => {
    const request = new Request("http://localhost:3000/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "Juan" }),
    });

    const response = await POST(request);
    expect(response.status).toBe(400);
  });
});

Organizacion de rutas: convenciones RESTful

El enrutamiento basado en archivos del App Router se mapea naturalmente a las convenciones de API RESTful. Organiza tus rutas siguiendo un patron consistente que haga tu API predecible y facil de navegar.

bash
# Estructura de rutas RESTful en el App Router
app/
  api/
    # Recurso: Proyectos
    projects/
      route.ts              # GET (listar), POST (crear)
      [id]/
        route.ts            # GET (leer), PUT (actualizar), DELETE (eliminar)
        publish/
          route.ts          # POST (accion personalizada)

    # Recurso: Usuarios
    users/
      route.ts              # GET (listar), POST (crear)
      [id]/
        route.ts            # GET, PUT, DELETE
        projects/
          route.ts          # GET (anidado: proyectos del usuario)

    # Endpoints sin recurso
    contact/
      route.ts              # Solo POST
    upload/
      route.ts              # Solo POST
    health/
      route.ts              # Solo GET (verificacion de salud)

Sigue estas convenciones para una API limpia y predecible:

  1. Usa sustantivos en plural para recursos: /api/projects, no /api/project.
  2. Usa metodos HTTP para operaciones: GET para leer, POST para crear, PUT para actualizar, DELETE para eliminar.
  3. Anida recursos relacionados: /api/users/[id]/projects para los proyectos de un usuario.
  4. Usa verbos para acciones no-CRUD: /api/projects/[id]/publish para publicar un proyecto.
  5. Devuelve codigos de estado apropiados: 200 para exito, 201 para creacion, 204 para eliminacion, 400 para entrada invalida, 401 para no autenticado, 404 para no encontrado, 500 para errores del servidor.
  6. Usa formas de respuesta consistentes: Siempre incluye un campo data para el exito y un campo error para los fallos.

Disenar rutas de API robustas en Next.js requiere pensar mas alla de simplemente manejar solicitudes. Al combinar validacion con Zod, manejo estructurado de errores, middleware de autenticacion, rate limiting y convenciones RESTful, creas una capa de API que es segura, mantenible y agradable de usar — tanto para los desarrolladores que la construyen como para los clientes que la consumen.

Compartir:

Artículos relacionados