Guia completo de CORS com Claude Code: APIs cross-origin seguras
Configure CORS com Claude Code: preflight, credentials, allowlist de origin, comandos de teste e prompts de revisão.
Configurar CORS corretamente com Claude Code
Um frontend em localhost:3000 e uma API em localhost:8787 já podem gerar erro de CORS no navegador. A correção rápida costuma ser adicionar Access-Control-Allow-Origin: *, mas isso fica perigoso quando a API usa cookies, header Authorization ou painel administrativo.
CORS, Cross-Origin Resource Sharing, é o mecanismo pelo qual o servidor informa quais outros origins podem ler suas respostas a partir de JavaScript no navegador. Um origin combina scheme, host e port. https://app.example.com, https://api.example.com, http://localhost:3000 e http://localhost:5173 são origins diferentes.
Este guia separa as decisões para que Claude Code ajude sem esconder os riscos. Você terá exemplos copiáveis para Express, Fastify, Cloudflare Workers e Next.js Route Handler, além de preflight, credentials, allowlist de origin, comandos de teste e prompts de revisão.
O ponto central: CORS não é autenticação. Ele só controla se JavaScript no navegador pode ler uma resposta cross-origin. Não bloqueia curl, chamadas servidor a servidor nem usuários sem permissão. Autenticação, autorização, CSRF, rate limiting e headers de segurança devem ser tratados separadamente.
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
Decisões antes do código
Defina estes valores antes de pedir uma configuração CORS ao Claude Code. Requisitos vagos tendem a gerar exemplos permissivos demais para produção.
| Decisão | Exemplo | Atenção |
|---|---|---|
| Origins permitidos | https://app.example.com, https://admin.example.com | Sem path e sem barra final |
| Credentials | Cookie, Authorization header | Com cookies, revise também SameSite=None; Secure |
| Métodos | GET,POST,PUT,PATCH,DELETE,OPTIONS | Permita só o que a API usa |
| Headers | Content-Type,Authorization,X-Request-ID | Devem bater com o preflight |
Preflight é a checagem que o navegador faz antes da requisição real. Para POST JSON, Authorization, PUT, DELETE e muitos headers customizados, ele envia OPTIONS primeiro. Sem Access-Control-Allow-Methods e Access-Control-Allow-Headers compatíveis, a requisição real não é enviada.
Configuração no Express
O exemplo assume Node.js 20 ou superior. O middleware oficial cors do Express aceita uma função em origin, permitindo validar cada request contra uma allowlist. Como a API usa credentials, ela reflete apenas origins permitidos e define 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");
});
Em produção, rode com NODE_ENV=production e mantenha apenas domínios reais em allowedOrigins. Requests sem header Origin não são CORS de navegador, por isso o exemplo permite a passagem; API keys, JWT e permissões ainda precisam ser validados no middleware de autenticação.
Configuração no Fastify
Fastify usa @fastify/cors. O README oficial aceita boolean, string, array, RegExp e function para origin, mas uma checagem exata com Set é mais simples de auditar. Evite regex amplas sem necessidade real.
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" });
No Fastify, a ordem de plugins e hooks importa. Se um hook de autenticação rejeitar OPTIONS antes do plugin CORS responder, o navegador nunca fará a requisição real. Peça ao Claude Code para revisar também a ordem de registro.
Configuração no Cloudflare Workers
Cloudflare Workers usa a API Fetch padrão. Trate OPTIONS explicitamente, adicione headers CORS em sucesso e erro, e use Vary: Origin quando a resposta variar por 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 });
},
};
O erro comum no Workers é adicionar headers apenas no caminho feliz. Se OPTIONS, 401, 403 ou 500 não tiverem CORS, o DevTools pode mostrar só uma falha de CORS e esconder o erro real.
Configuração no Next.js Route Handler
Com App Router, app/api/.../route.ts usa Request e Response padrão da Web. A documentação do Next.js mostra como adicionar CORS a uma resposta; para APIs com credentials, use allowlist em 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() no next.config.js serve para headers estáticos de APIs públicas. Quando o origin precisa ser avaliado por request, a lógica no Route Handler fica mais clara.
Comandos de teste
Use curl para separar preflight e request real. Confirme se Access-Control-Allow-Origin corresponde exatamente ao Origin enviado e se Access-Control-Allow-Credentials: true aparece apenas 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"
No navegador, um teste com credentials fica assim. Se credentials: "include" for usado, uma resposta CORS com wildcard será rejeitada pelo navegador.
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 reais
O primeiro caso é uma SPA e uma API em domínios separados. Se o React está em https://app.example.com e a API em https://api.example.com, uma allowlist explícita é necessária. Com cookies de login, revise credentials, atributos de cookie e CSRF juntos.
O segundo caso é um frontend administrativo. Você pode adicionar https://admin.example.com à allowlist, mas CORS não substitui autorização de administrador. Essa lógica fica na API.
O terceiro caso é um Cloudflare Worker como BFF ou proxy leve. O navegador chama o Worker, e o Worker chama a API upstream. A resposta do Worker para o navegador ainda precisa de CORS correto.
O quarto caso é uma API pública somente leitura. Sem cookies, Authorization ou dados privados, Access-Control-Allow-Origin: * pode ser aceitável. Se autenticação pode surgir depois, comece com allowlist.
Armadilhas específicas
| Armadilha | Resultado | Correção |
|---|---|---|
Combinar * com credentials: true | Navegador bloqueia a resposta | Retorne o origin explícito |
Registrar https://app.example.com/ | A barra final impede match | Guarde https://app.example.com |
Permitir só localhost | Portas diferentes falham | Use http://localhost:3000 |
Exigir auth em OPTIONS | Preflight para em 401/403 | Trate preflight antes da auth |
| Sem CORS em erros | DevTools esconde o erro real | Adicione headers em 4xx/5xx |
| CDN cacheia headers por origin | Headers se misturam | Adicione Vary: Origin |
| Tratar CORS como autorização | Clientes não navegador continuam chamando | Implemente auth e CSRF à parte |
O MDN é claro: requests CORS com credentials não podem usar Access-Control-Allow-Origin: *. Se Claude Code gerar essa combinação, trate como bug.
Prompts de revisão 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.
Referências e links internos
Use o guia de CORS do MDN como base. Para implementação, consulte Express cors middleware, @fastify/cors, exemplos CORS do Cloudflare Workers e Next.js Route Handlers. Para fluxos reutilizáveis, veja Claude Code commands.
Leia também o guia de desenvolvimento de API, o guia de headers de segurança web, o guia de Cloudflare Workers e a checklist de revisão de código.
Próximo passo
Depois de trocar os domínios de exemplo pelos seus, rode os prompts de revisão no repositório e use o guia de boas práticas de segurança com Claude Code para revisar cookies, CSRF, autorização e headers em conjunto. Em projetos de cliente ou plataformas internas, essa checklist pode virar artefato de revisão e apoiar propostas de implementação paga ou adoção de templates reutilizáveis.
Resultado ao testar na prática
No teste local de Masa, os exemplos de Express e Fastify rodaram em localhost:8787; preflight e POST de Origin: http://localhost:3000 funcionaram, enquanto https://evil.example retornou 403. Os pontos mais fáceis de esquecer foram CORS em respostas de erro e tratamento explícito de OPTIONS no Workers. O fluxo mais estável foi implementar a allowlist, rodar os curl e pedir ao Claude Code para confirmar que não existe wildcard com credentials, que Vary: Origin aparece onde precisa e que localhost não entra em produção.
Artigos relacionados
Security headers web com Claude Code: CSP, nonce, HSTS e deploy sem quebrar anúncios
Configure CSP, nonce, HSTS, frame-ancestors e security headers com Claude Code em Next.js, Astro, Express e Cloudflare.
Web scraping seguro com Claude Code: Fetch, Playwright e logs de auditoria
Implemente web scraping seguro com Claude Code: robots.txt, Fetch, Playwright, CSV e auditoria.
Como Construir um Player de Áudio com Claude Code
Aprenda a construir um player de áudio usando o Claude Code. Inclui exemplos práticos de código e orientação passo a passo.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.