Um erro de CORS ("No 'Access-Control-Allow-Origin' header is present") significa que uma página web em uma origem, como http://localhost:5173, chamou sua API em outra origem, e o navegador não deixou a página ler a resposta porque a resposta não dizia que aquela origem era permitida. A correção é no servidor: envie Access-Control-Allow-Origin com a origem da página, e responda a requisição de preflight OPTIONS do navegador com os métodos e headers que você aceita. No Express isso são poucas linhas de middleware, ou app.use(cors()) do pacote cors.
A resposta é a lista de produtos, e a aba Headers mostra access-control-allow-origin: https://app.example.com: uma página nessa origem pode lê-la. Agora envie OPTIONS /api/products/1 com os headers Origin: https://app.example.com e Access-Control-Request-Method: PUT. Esse é o preflight que um navegador envia antes de um PUT, e a resposta é 204 com os métodos e headers permitidos e sem body.
O que o navegador confere
Páginas de mesma origem não precisam de nada. Para uma requisição entre origens, o navegador lê estes headers da resposta:
| Header | Significado |
|---|---|
Access-Control-Allow-Origin | A única origem que pode ler a resposta, ou * para qualquer uma |
Access-Control-Allow-Methods | Métodos permitidos na requisição real (só no preflight) |
Access-Control-Allow-Headers | Headers que a página pode enviar, como Authorization (só no preflight) |
Access-Control-Allow-Credentials | true se a página pode enviar cookies e ler a resposta |
Access-Control-Max-Age | Segundos que o navegador pode guardar a resposta do preflight em cache |
Vary: Origin | Avisa os caches que a resposta depende do header Origin da requisição |
Um GET simples ou um POST no estilo de formulário vai direto para o servidor, e o navegador confere Access-Control-Allow-Origin na resposta. O servidor executa a requisição mesmo assim; o CORS só decide se a página vê o resultado. Uma requisição com body JSON, com header Authorization ou com um método como PUT ou DELETE é precedida por uma requisição de preflight OPTIONS, e a requisição real só é enviada se a resposta do preflight permitir. Por isso um middleware de CORS precisa responder o OPTIONS antes de qualquer rota ou verificação de autenticação rodar.
Liberar várias origens, com credentials
Access-Control-Allow-Origin guarda uma origem ou *, nunca uma lista. Para liberar várias, compare o header Origin da requisição com uma lista de permitidas e devolva o mesmo valor quando ele estiver nela:
A primeira requisição não tem header Origin, como uma chamada do curl ou de uma página de mesma origem, então ela recebe {"origin":null} e nenhum header de CORS. Envie GET /api/me com o header Origin: http://localhost:5173 e a aba Headers mostra essa origem devolvida mais access-control-allow-credentials: true. Troque o header para Origin: https://evil.example e os dois headers de CORS somem: o servidor ainda responde, mas um navegador impediria a página de ler. Access-Control-Allow-Credentials: true deixa a página enviar cookies (com fetch(url, { credentials: 'include' })), e os navegadores o rejeitam junto com *, então credentials sempre exige a origem exata.
O pacote cors
O pacote cors faz o mesmo trabalho com opções. Instale com npm install cors:
const cors = require('cors');
// Any origin, no cookies: fine for a public read-only API.
app.use(cors());
// Only your frontend, with cookies.
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// One route only.
app.get('/api/public', cors(), handler);
Chamado sem opções, cors() envia Access-Control-Allow-Origin: * e responde todo preflight. Use * só para dados públicos que não exigem login.
Erros comuns
Adicionar CORS depois das rotas ou depois de um middleware de autenticação. A requisição de preflight OPTIONS não leva header Authorization, então uma verificação de autenticação colocada antes responde com 401 e o navegador informa um erro de CORS. Registre o middleware de CORS primeiro.
Usar * com credentials. Os navegadores recusam a resposta. Devolva a origem específica.
Tentar resolver CORS no frontend. Opções de requisição como mode: 'no-cors' não removem a restrição; elas te dão uma resposta opaca que você não consegue ler. Só os headers do servidor resolvem.
Para entender como funcionam o app.use() e a ordem dos middlewares, veja middleware no Express.
Perguntas frequentes
Por que minha API funciona no Postman ou no curl, mas não no navegador?
O CORS só é aplicado pelos navegadores. Postman, curl e outros servidores ignoram os headers de CORS, então leem a resposta de qualquer jeito. O navegador se recusa a entregar a resposta para scripts de uma página de outra origem, a menos que o servidor permita essa origem.
CORS é um recurso de segurança do meu servidor?
Ele protege os usuários, não o seu servidor. Ele impede que uma página maliciosa leia respostas da sua API com os cookies do usuário. Qualquer um ainda pode chamar sua API a partir de um script ou de um servidor, então autenticação e autorização continuam sendo trabalho seu.
O que conta como outra origem?
Uma origem é esquema, host e porta juntos. http://localhost:5173 e http://localhost:3000 são origens diferentes porque as portas mudam, e https://example.com e https://api.example.com mudam no host.
Dá para evitar CORS em desenvolvimento?
Sim, tornando a requisição de mesma origem: configure o servidor de desenvolvimento do frontend (o server.proxy do Vite, por exemplo) para encaminhar /api para o servidor Express. O navegador então fala com uma única origem e nenhum header de CORS é necessário.