HTTP security headers are instructions your app sends with every response that tell the browser to be stricter: only use HTTPS, don't guess file types, don't let other sites frame your pages, and only run scripts from places you trust. They take an afternoon to set up, cost nothing in performance and close off whole classes of attack. This guide covers the headers worth setting, how to add them in Next.js and Express, and how to roll out the strict ones without breaking your site.
The headers that matter
| Header | What it does | A sensible value |
|---|---|---|
Strict-Transport-Security |
Tells browsers to use HTTPS only for your domain, even if someone types http:// |
max-age=31536000; includeSubDomains |
Content-Security-Policy |
Limits where scripts, styles, images and frames may load from | Built per site, see below |
X-Content-Type-Options |
Stops browsers guessing a file's type, which blocks some script injection tricks | nosniff |
Referrer-Policy |
Controls how much of the current URL is sent to other sites when users click links | strict-origin-when-cross-origin |
Permissions-Policy |
Switches off browser features you don't use, such as camera or geolocation | camera=(), microphone=(), geolocation=() |
X-Frame-Options |
Stops other sites from showing your pages in a frame (clickjacking) | DENY or SAMEORIGIN |
You may see older advice to set X-XSS-Protection. Modern browsers have removed that filter, and a Content Security Policy does the job properly, so you can leave it out.
It is also worth removing headers that only help attackers. Next.js sends X-Powered-By: Next.js and Express sends X-Powered-By: Express unless you turn them off.
Adding headers in Next.js
Next.js lets you set response headers for any path in next.config.ts:
import type { NextConfig } from "next";
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{ key: "X-Frame-Options", value: "DENY" },
];
const nextConfig: NextConfig = {
poweredByHeader: false,
async headers() {
return [{ source: "/:path*", headers: securityHeaders }];
},
};
export default nextConfig;
These headers apply to pages, API routes and static files alike. If one route genuinely needs to be framed, for example an embeddable widget, add a more specific rule for that path with SAMEORIGIN or a CSP frame-ancestors list, rather than weakening the whole site.
Adding headers in Express
For an Express API or server-rendered app, the Helmet middleware (npm install helmet) sets a sensible collection of security headers in one line, including HSTS, nosniff, frame protection and a default Content Security Policy, and removes X-Powered-By:
import express from "express";
import helmet from "helmet";
const app = express();
app.use(helmet());
app.get("/health", (req, res) => res.json({ ok: true }));
app.listen(process.env.PORT || 3000);
Helmet's default CSP is strict. For a pure JSON API that's ideal. For an app that serves HTML with third-party scripts, configure the CSP directives yourself, as described next.
Content Security Policy without breaking your site
A Content Security Policy (CSP) is the most powerful header and the one most likely to break something, because it blocks any script, style, font, image or connection you didn't list. MDN's Content Security Policy reference documents every directive. A typical starting point for a site that loads nothing from other domains looks like this:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
Send it as one line, with the directives separated by semicolons. Some notes:
'unsafe-inline'for scripts is needed by many Next.js setups, because the framework adds small inline scripts to each page. The stricter alternative is a nonce generated per request, which the Next.js Content Security Policy guide explains. Nonces make every page dynamic, so weigh that against your caching.- Third parties must be listed. Analytics, chat widgets, payment pages, embedded maps and video each need their domains in the right directive. List them deliberately; that list doubles as an inventory of who can run code on your site.
frame-ancestors 'none'is the modern equivalent ofX-Frame-Options: DENY. Setting both is fine.
Roll out in report-only mode first
Instead of switching the policy on straight away, send it as Content-Security-Policy-Report-Only. The browser then reports violations in the developer console (and to a reporting endpoint if you configure one) but blocks nothing. Click through your whole site, including forms, checkout and pages with embeds, fix the policy until the console is clean, then rename the header to Content-Security-Policy.
Be careful with HSTS
Strict-Transport-Security is excellent, and it is the one header you cannot quickly undo. Once a browser has seen it, it refuses plain HTTP for your domain until max-age expires. Before you set it:
- Make sure every page works over HTTPS, and that HTTP redirects to HTTPS. Our guide to www or non-www and HTTPS redirects covers the redirect side.
- If you add
includeSubDomains, every subdomain must have a valid certificate too, including old ones like a forgottentest.site. Check your DNS records first. - Consider starting with a short
max-age(for example a day) and raising it once you are confident. - Only add
preloadand submit your domain to the browser preload list if you understand that removal takes a long time.
Free Let's Encrypt certificates make HTTPS on every hostname straightforward. If you're unsure how certificates work, read SSL certificates explained.
Checking your headers
From a terminal:
curl -sI https://www.example.co.za/ | grep -iE "strict-transport|content-security|x-content-type|referrer-policy|permissions-policy|x-frame"
Or open your browser's developer tools, go to the Network tab, click the first document request and look at the response headers. Check a page, an API route and a static file, because some setups apply headers to only one of them.
Avoid duplicate headers
If a reverse proxy or the hosting panel already adds a header and your app adds it again, browsers may see two values. For CSP, two policies both apply, so the result is stricter than either. Pick one place to set each header, ideally your application code, where it is versioned with everything else.
Where headers fit in your security work
Headers protect users' browsers. They don't fix vulnerable dependencies, weak passwords or missing access checks in your code. Use them alongside the rest of our Node.js security checklist and the items in the Next.js production checklist.
Frequently asked questions
Do security headers affect SEO or speed?
Not measurably. They are a few hundred bytes per response. HTTPS itself is a ranking signal, and HSTS helps make sure visitors always land on the HTTPS version.
Which header should I add first?
X-Content-Type-Options, Referrer-Policy and frame protection are safe to add straight away. Add HSTS once HTTPS works everywhere, and CSP last, in report-only mode first.
Can I set headers in the hosting control panel instead of my code?
Often you can, but setting them in the app keeps them in Git, makes them apply the same way on every environment and avoids duplicates. Use one place, not both.
Why does my CSP block Google Analytics or a chat widget?
Because their script and connection domains are not in your policy. Add the domains the provider documents to script-src, connect-src and, if needed, img-src and frame-src.
Run your Next.js or Node.js app on NewHost: South African servers, free Let's Encrypt SSL on your own domain and Git deploys, so the headers you set in code ship with every push.