GuidesNODE.JS
Fix CORS errors in Fastify
A Fastify API sends no cross-origin resource sharing headers on its own, so browsers withhold its responses from scripts running on other origins. Resolving fastify cors failures means registering the official plugin, then configuring origin matching, allowed methods, and credentials to fit your frontend.
Confirm it is a CORS problem
Inspect the browser console for messages referencing the Access-Control-Allow-Origin header before changing server settings. A failing preflight, a 500 on the OPTIONS route, and a plain network drop can look alike in application code. Check the diagnosis guide to confirm the error comes from browser policy first.
Install @fastify/cors
The official plugin is @fastify/cors on npm. Plugin version 11 pairs with Fastify 5; older Fastify majors have their own plugin lines.
$ npm i @fastify/cors
# Plugin v11 pairs with Fastify v5Enable CORS for all routes
import Fastify from 'fastify';
import cors from '@fastify/cors';
const fastify = Fastify();
// Registered first, every route below gets the CORS hooks
await fastify.register(cors, { origin: true });
fastify.get('/products/:id', (req, reply) => {
reply.send({ msg: 'Hello' });
});
await fastify.listen({ port: 3000 });Register the plugin before declaring your routes. Fastify applies plugins in registration order, so routes defined before the registration miss the CORS hooks and the preflight route.
Registered with no options, the origin default is *. Passing { origin: true } reflects the incoming request origin back to the caller instead.
Restrict origins in production
await fastify.register(cors, {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'],
});
// methods defaults to GET,HEAD,POST only, so PUT and DELETE
// fail preflight unless you list them here.The * default and origin: true both let any website read your API from a browser. For production APIs serving specific frontends, configure origin with an exact string, a RegExp, an array of strings and RegExps, or a function with signature (origin, cb) where the callback is cb(err, origin). Async functions are supported.
The methods option defaults strictly to GET,HEAD,POST. If your API accepts PUT, PATCH, or DELETE, define the methods array explicitly or preflights for those methods fail.
How preflight requests are handled
Registration adds an onRequest hook and a wildcard OPTIONS route to the instance. Browser preflights are answered by that route automatically and never reach your handlers. preflightContinue defaults to false; setting it to true forwards the preflight to the route handler, and preflight: false disables automatic preflight handling entirely.
With strictPreflight at its default of true, preflights missing the Origin or Access-Control-Request-Method headers get a 400 response. allowedHeaders defaults to reflecting whatever the browser lists in Access-Control-Request-Headers, maxAge sets Access-Control-Max-Age in seconds, and optionsSuccessStatus changes the default 204 for legacy browsers that choke on it.
Sending cookies or Authorization
await fastify.register(cors, {
origin: 'https://app.example.com',
credentials: true,
});
// Browser side, cookies only flow when requested:
await fetch('https://api.example.com/account', { credentials: 'include' });
// axios: axios.get(url, { withCredentials: true })Setting credentials: true adds the Access-Control-Allow-Credentials: true response header. Browsers reject any credentialed response that pairs it with Access-Control-Allow-Origin: *, so the resolved origin must be an explicit string when credentials are enabled.
The client must opt in as well. Pass credentials: 'include' to fetch, or set withCredentials: true in axios, or the browser never attaches cookies.
Verify the fix
$ curl -i -H "Origin: https://app.example.com" http://localhost:3000/products/1
HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com
$ curl -i -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: POST" http://localhost:3000/products
HTTP/1.1 204 No Content
access-control-allow-origin: https://app.example.com
access-control-allow-methods: GET, HEAD, POST, PUT, PATCH, DELETEConfirm that Access-Control-Allow-Origin matches your frontend origin, that the OPTIONS probe returns a 204 with the requested method listed in Access-Control-Allow-Methods, and that Access-Control-Allow-Credentials is true when cookies or Authorization headers are in play. The header checker evaluates pasted headers against browser rules.
Good questions.
Does CORS block requests on the Fastify server side?
No. CORS is enforced by the browser. Non-preflighted requests still reach Fastify and execute normally; the browser only blocks your frontend code from reading the response when the headers are missing.
Why do PUT, PATCH, and DELETE requests fail preflight checks?
The methods option defaults to the CORS-safelisted set GET,HEAD,POST only. Register the plugin with methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'] or a similar list that covers your API.
Does registration order matter for @fastify/cors?
Yes. Register @fastify/cors before defining application routes. Routes registered earlier do not inherit the CORS hook or the wildcard OPTIONS route.
Can I override CORS settings on a single route?
Yes. Route options accept config.cors, for example fastify.get('/public', { config: { cors: { origin: '*' } } }, handler). Setting config: { cors: false } disables CORS handling for that route.