Security headers and rate limiting
Two simple steps noticeably improve an API's protection. The first is protective response headers: they tell the browser what must not be done with your site. The second is rate limiting: one client cannot send thousands of requests a minute, for example guessing passwords on /login.
Installing
npm install @fastify/helmet @fastify/rate-limitHelmet
The @fastify/helmet plugin adds a set of protective headers to every reply. Register it in src/app.js:
// src/app.js
import helmet from '@fastify/helmet';
app.register(helmet);Run curl -i http://localhost:3000/posts — the headers now include, for example, x-content-type-options: nosniff and content-security-policy.
| Header | What it forbids |
|---|---|
Content-Security-Policy | Loading scripts and styles from foreign addresses |
X-Content-Type-Options | The browser guessing the file type |
X-Frame-Options | Embedding the site in a foreign iframe |
Strict-Transport-Security | Accessing the site without HTTPS |
For a pure JSON API the defaults are fine. If the server also serves HTML pages, the contentSecurityPolicy policy is tuned to your scripts.
Rate limiting
The @fastify/rate-limit plugin counts requests from each IP address and, once the limit is exceeded, answers with 429 "Too Many Requests":
// src/app.js
import rateLimit from '@fastify/rate-limit';
app.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
});Now one address can make at most 100 requests a minute. Each reply has the x-ratelimit-limit and x-ratelimit-remaining headers — the limit and how many requests are left.
A route's own limit
The login route deserves a stricter limit. It is set in the route's config option in src/routes/auth.js:
// src/routes/auth.js
app.post('/login', {
config: {
rateLimit: { max: 5, timeWindow: '1 minute' },
},
}, async (request) => {
// ...check the login and issue a token
});The sixth login attempt within a minute gets a 429 reply, which makes guessing passwords pointless.
Registration order
Register both plugins in src/app.js before the routes: rate-limit applies limits to routes declared after it.
Common mistake
Behind a reverse proxy (such as nginx) all requests come from one address — the proxy's — and the limit triggers for all visitors at once. Turn on the trustProxy: true option when creating the application so Fastify takes the real client IP from the X-Forwarded-For header.