How to Set Up the next.config.js headers() Array

This guide is part of the Vercel & Next.js Security Header Management reference. It covers the precise syntax of the next.config.js headers() async function — the build-time mechanism for emitting static security headers like HSTS and Content-Security-Policy — and the route-matching, App-vs-Pages, and static-asset edge cases that cause it to silently misfire.

Configuration Syntax & Exact Values

headers() is an async export that returns an array. Each element is an object with a source (a Next.js path pattern, not raw regex) and a headers array of { key, value } objects. The function runs at build time, so every value must be a static string — it cannot read the request and cannot mint a per-request nonce.

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // source: Next.js path-matching syntax. '/(.*)' matches every route.
        source: '/(.*)',
        headers: [
          // 1 year. includeSubDomains extends the policy to every subdomain.
          { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
          // nosniff: forces strict MIME checking, blocking content-sniffing attacks.
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          // DENY: page may not be framed anywhere; legacy clickjacking control.
          { key: 'X-Frame-Options', value: 'DENY' },
          // Send only the origin on cross-origin navigations.
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        ],
      },
    ];
  },
};

Annotated breakdown of each field:

To scope headers to a subset of routes, add more objects with narrower source patterns. More-specific entries do not override broader ones; all matching entries apply, so overlapping source patterns that set the same key produce duplicates.

// next.config.js — route-scoped headers
module.exports = {
  async headers() {
    return [
      {
        source: '/api/(.*)',
        headers: [
          { key: 'Content-Security-Policy', value: "default-src 'none'; frame-ancestors 'none'" },
          { key: 'Cache-Control', value: 'no-store' },
        ],
      },
      {
        // Long-lived caching for hashed, immutable build assets.
        source: '/_next/static/(.*)',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
        ],
      },
    ];
  },
};
How a source pattern maps headers onto matching routes A source pattern in next.config.js tests each requested route and applies its headers array only to routes that match. source: '/api/(.*)' match? /api/users /api/orders /dashboard /_next/static/x.js yes no headers array applied to matched routes
A source pattern tests each requested route; the headers array is applied only to routes that match.

Server-Side Configuration

next.config.js

Place the headers() export in next.config.js (or next.config.mjs with export default). It applies to server-rendered and statically-generated routes alike, on both the App Router and the Pages Router.

// next.config.mjs (ES module form)
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
        ],
      },
    ];
  },
};
export default nextConfig;

Note: vercel.json alternative

If the deployment is a static export (output: 'export'), headers() is dropped at build — there is no server to run it. On Vercel, move these same keys into the vercel.json headers array, which the platform router applies to every path including static files. Do not define a key in both next.config.js and vercel.json on a server-rendered build, or the response carries duplicates. The full trade-off is in the vercel.json vs next.config.js comparison.

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Diagnostic & Verification Steps

headers() is evaluated only by next build and is silently ignored under next dev. Always verify against a production build.

# 1. Build and start the production server.
npm run build && npm start

# 2. Inspect the raw response headers on the root route.
curl -sI http://localhost:3000/

Expected output (casing normalized to lowercase by Next.js; order may vary):

HTTP/1.1 200 OK
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()

Verify scoped entries on the paths they target, and confirm a security key appears exactly once:

# Route-scoped headers on an API path.
curl -sI http://localhost:3000/api/test | grep -iE 'content-security|cache-control'

# Confirm no duplicate HSTS line (expected: 1).
curl -sI http://localhost:3000/ | grep -ic 'strict-transport-security'

If a header is absent, the build did not pick up the config: confirm you ran next build (not next dev), and that output: 'export' is not set.

Edge Cases, Security Implications & Safe Rollback

Frequently Asked Questions

Why are my headers ignored when I run the dev server?

next dev does not evaluate the headers() export. Only next build does. Verify with npm run build && npm start or on a deployed preview.

Can I put a CSP nonce in the headers() array?

No. headers() runs at build time with no access to the request, so every value is a static string. Per-request nonces must be set in middleware. See Setting CSP with nonces in Next.js middleware.

Why do two source patterns produce duplicate headers?

All matching source entries apply — more-specific patterns do not override broader ones. If two entries set the same key for a route, the response carries both. Scope your patterns so each key is set once per route.

Does headers() work the same on the App Router and Pages Router?

Yes. headers() is config-level and router-agnostic. The only relevant difference is that neither router can generate CSP nonces from config — that requires middleware.

Conclusion

Use the headers() array for static, route-scoped security headers that need no per-request context, and validate every change against a production build (next build && next start) since next dev ignores the export entirely. Roll out incrementally: stage with a short Strict-Transport-Security max-age, confirm the headers on a preview deploy with curl -sI, then promote to full production values. For anything per-request, move to middleware.