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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.