Use Cases (Actualizado: 2/6/2026)

Guía completa de CORS con Claude Code: APIs cross-origin seguras

Configura CORS con Claude Code: preflight, credentials, allowlist de origin, comandos de prueba y prompts de revisión.

Guía completa de CORS con Claude Code: APIs cross-origin seguras

Configurar CORS correctamente con Claude Code

Con un frontend en localhost:3000 y una API en localhost:8787, el navegador ya puede mostrar un error de CORS. La solución rápida suele ser añadir Access-Control-Allow-Origin: *, pero esa configuración es peligrosa cuando la API usa cookies, cabeceras Authorization o un panel de administración.

CORS, Cross-Origin Resource Sharing, es el mecanismo con el que el servidor indica qué otros origins pueden leer sus respuestas desde código JavaScript ejecutado en el navegador. Un origin combina scheme, host y port. https://app.example.com, https://api.example.com, http://localhost:3000 y http://localhost:5173 son origins distintos.

Esta guía separa las decisiones para que Claude Code pueda ayudar sin ocultar los riesgos. Incluye ejemplos copiables para Express, Fastify, Cloudflare Workers y Next.js Route Handler, además de preflight, credentials, allowlist de origin, comandos de prueba y prompts de revisión.

La idea clave: CORS no es autenticación. Solo controla si JavaScript en el navegador puede leer una respuesta cross-origin. No bloquea curl, llamadas servidor a servidor ni usuarios sin permisos. Autenticación, autorización, CSRF, rate limiting y cabeceras de seguridad deben diseñarse aparte.

sequenceDiagram
  participant Browser as Browser
  participant API as API server
  Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
  API-->>Browser: 204 + Access-Control-Allow-*
  Browser->>API: POST /api/messages<br/>Cookie or Authorization
  API-->>Browser: 200 + Access-Control-Allow-Origin

Decisiones antes de escribir código

Define estos valores antes de pedirle a Claude Code que cree la configuración. Los requisitos vagos suelen producir ejemplos demasiado permisivos para producción.

DecisiónEjemploAtención
Origins permitidoshttps://app.example.com, https://admin.example.comSin rutas ni barra final
CredencialesCookie, Authorization headerCon cookies revisa también SameSite=None; Secure
MétodosGET,POST,PUT,PATCH,DELETE,OPTIONSPermite solo los que uses
CabecerasContent-Type,Authorization,X-Request-IDDeben coincidir con el preflight

El preflight es la comprobación que hace el navegador antes de la petición real. Para POST JSON, Authorization, PUT, DELETE y muchas cabeceras personalizadas, primero envía OPTIONS. Si la respuesta no incluye Access-Control-Allow-Methods y Access-Control-Allow-Headers compatibles, la petición real no se envía.

Configuración en Express

El ejemplo asume Node.js 20 o superior. El middleware oficial cors de Express acepta una función en origin, lo que permite validar cada request contra una allowlist. Como la API usa credentials, solo refleja origins permitidos y activa credentials: true.

npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";

const app = express();

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

const corsOptions = {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  optionsSuccessStatus: 204,
};

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return res.status(403).json({ error: "Origin not allowed" });
  }
  next();
});

app.use(cors(corsOptions));
app.use(express.json());

app.get("/api/health", (_req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true });
});

app.post("/api/messages", (req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true, received: req.body });
});

app.listen(8787, () => {
  console.log("API listening on http://localhost:8787");
});

En producción, ejecuta con NODE_ENV=production y deja solo dominios reales en allowedOrigins. Las requests sin header Origin no son CORS de navegador, por eso el ejemplo las deja pasar; API keys, JWT y permisos de usuario siguen siendo responsabilidad del middleware de autenticación.

Configuración en Fastify

Fastify usa @fastify/cors. Su README oficial permite booleanos, strings, arrays, expresiones regulares y funciones para origin, pero una coincidencia exacta con Set es más fácil de auditar. Evita regex amplias salvo que tengas una razón clara.

npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";

const app = Fastify({ logger: true });

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

app.addHook("onRequest", async (request, reply) => {
  const origin = request.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return reply.code(403).send({ error: "Origin not allowed" });
  }
});

await app.register(cors, {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  strictPreflight: true,
});

app.get("/api/health", async () => ({ ok: true }));

app.post("/api/messages", async (request) => {
  return { ok: true, received: request.body };
});

await app.listen({ port: 8787, host: "0.0.0.0" });

En Fastify importa el orden de plugins y hooks. Si un hook de autenticación rechaza OPTIONS antes de que responda el plugin de CORS, el navegador nunca enviará la petición real. Pide a Claude Code que revise también el orden de registro.

Configuración en Cloudflare Workers

Cloudflare Workers expone la API estándar Fetch. Maneja OPTIONS de forma explícita, añade cabeceras CORS tanto en éxito como en error, y usa Vary: Origin cuando la respuesta cambia según el origin.

// src/index.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export default {
  async fetch(request: Request): Promise<Response> {
    const corsHeaders = getCorsHeaders(request);
    if (corsHeaders === null) {
      return Response.json({ error: "Origin not allowed" }, { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders });
    }

    const url = new URL(request.url);
    if (url.pathname === "/api/messages" && request.method === "POST") {
      const body = await request.json().catch(() => ({}));
      return Response.json({ ok: true, received: body }, { headers: corsHeaders });
    }

    return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
  },
};

El error habitual en Workers es poner cabeceras solo en el camino exitoso. Si OPTIONS, 401, 403 o 500 no tienen CORS, DevTools puede mostrar solo un fallo CORS y ocultar el error real de la aplicación.

Configuración en Next.js Route Handler

Con App Router, app/api/.../route.ts usa Request y Response estándar. La documentación de Next.js muestra cómo añadir CORS a una respuesta; para APIs con credentials, usa allowlist en vez de *.

// app/api/messages/route.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export async function OPTIONS(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }
  return new Response(null, { status: 204, headers });
}

export async function POST(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }

  const body = await request.json().catch(() => ({}));
  return Response.json({ ok: true, received: body }, { headers });
}

headers() en next.config.js sirve para cabeceras estáticas de APIs públicas. Si el origin debe evaluarse por request, es más claro hacerlo dentro del Route Handler.

Comandos de prueba

Usa curl para separar preflight y petición real. Comprueba que Access-Control-Allow-Origin coincide exactamente con el Origin enviado y que Access-Control-Allow-Credentials: true aparece solo para origins permitidos.

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

curl -i -X POST http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dev-token" \
  --data '{"text":"hello"}'

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: POST"

En el navegador, una prueba con credentials se ve así. Si usas credentials: "include", el navegador rechazará una respuesta CORS con wildcard.

await fetch("http://localhost:8787/api/messages", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer dev-token",
  },
  body: JSON.stringify({ text: "hello" }),
});

Casos de uso reales

El primer caso es una SPA y una API en dominios distintos. Si React vive en https://app.example.com y la API en https://api.example.com, necesitas una allowlist explícita. Con cookies de login, revisa credentials, atributos de cookie y CSRF juntos.

El segundo caso es un frontend de administración. Puedes añadir https://admin.example.com a la allowlist, pero CORS no sustituye la autorización de administrador. Esa lógica debe estar en la API.

El tercer caso es un Cloudflare Worker como BFF o proxy ligero. El navegador habla con el Worker, y el Worker llama al upstream. La respuesta del Worker hacia el navegador sigue necesitando CORS correcto.

El cuarto caso es una API pública de solo lectura. Si no tiene cookies, Authorization ni datos privados, Access-Control-Allow-Origin: * puede ser aceptable. Si habrá autenticación en el futuro, empieza con allowlist.

Errores concretos

ErrorResultadoCorrección
Combinar * con credentials: trueEl navegador bloquea la respuestaDevuelve el origin explícito
Registrar https://app.example.com/La barra final rompe la coincidenciaGuarda https://app.example.com
Permitir solo localhostFallan puertos distintosUsa http://localhost:3000
Exigir auth para OPTIONSPreflight se detiene en 401/403Procesa preflight antes de auth
Omitir CORS en erroresDevTools oculta el error realAñade cabeceras a 4xx/5xx
CDN cachea cabeceras por originSe mezclan cabeceras entre originsAñade Vary: Origin
Tratar CORS como autorizaciónClientes no navegador siguen llamandoImplementa auth y CSRF aparte

MDN es claro: las requests CORS con credentials no pueden usar Access-Control-Allow-Origin: *. Si Claude Code genera esa combinación, trátala como un bug.

Prompts de revisión para Claude Code

Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>

curl preflight:
<paste curl -i -X OPTIONS output>

Expected origin:
https://app.example.com

Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.

Referencias y enlaces internos

Usa la guía de CORS de MDN como base. Para implementar, consulta Express cors middleware, @fastify/cors, ejemplos CORS de Cloudflare Workers y Next.js Route Handlers. Para flujos reutilizables en Claude Code, revisa Claude Code commands.

También conviene leer la guía de desarrollo de API, la guía de cabeceras de seguridad web, la guía de Cloudflare Workers y la checklist de revisión de código.

Siguiente paso

Después de sustituir los dominios de ejemplo por los tuyos, ejecuta los prompts de revisión en tu repositorio y usa la guía de buenas prácticas de seguridad con Claude Code para revisar cookies, CSRF, autorización y cabeceras en conjunto. En proyectos de cliente o plataformas internas, esta checklist puede convertirse en un artefacto de revisión que facilita proponer soporte de implementación o plantillas reutilizables.

Resultado de probarlo en la práctica

En la prueba local de Masa, los ejemplos de Express y Fastify corrieron en localhost:8787; el preflight y el POST desde Origin: http://localhost:3000 funcionaron, mientras https://evil.example devolvió 403. Los olvidos más fáciles fueron las cabeceras CORS en errores y el manejo explícito de OPTIONS en Workers. El flujo más estable fue implementar la allowlist, ejecutar los curl, y pedir a Claude Code que confirme que no existe la combinación wildcard más credentials, que Vary: Origin aparece donde corresponde y que localhost no llega a producción.

#Claude Code #CORS #security #API #web development
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.