Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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-limit

Helmet

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.

HeaderWhat it forbids
Content-Security-PolicyLoading scripts and styles from foreign addresses
X-Content-Type-OptionsThe browser guessing the file type
X-Frame-OptionsEmbedding the site in a foreign iframe
Strict-Transport-SecurityAccessing 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.

Thanks for reading!

Donate to the site — we work hard for you.