Security isn't a feature you bolt on at the end — it's a mindset that shapes every architectural decision. After building several production Next.js applications and completing multiple cybersecurity certifications, I've distilled the most impactful patterns into this guide.
This article assumes familiarity with Next.js App Router and basic TypeScript. All examples use Next.js 14+ with the App Router.
Why Security Matters
The average web application faces thousands of automated attacks daily. Cross-site scripting (XSS), SQL injection, and credential stuffing are not theoretical threats — they're the baseline. Next.js provides some built-in protections, but they're not enough on their own.
A single misconfigured API route can expose your entire database. A missing CSP header can let attackers inject scripts into your users' browsers. Security requires deliberate, layered defense.
Authentication Patterns
The first line of defense is knowing who your users are. In Next.js, authentication touches both the server and client, and getting it wrong in either layer creates vulnerabilities.
JWT vs Session-Based Auth
JWTs are stateless and scale well, but they can't be revoked without a deny-list. Server-side sessions offer more control but require a store like Redis. For most applications, I recommend a hybrid: short-lived JWTs (15 min) with refresh tokens stored in httpOnly cookies.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const payload = await verifyToken(token);
const headers = new Headers(request.headers);
headers.set("x-user-id", payload.sub);
return NextResponse.next({ headers });
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};Never store JWTs in localStorage. Use httpOnly, secure, sameSite cookies instead. LocalStorage is accessible to any JavaScript running on your page — including injected scripts.
Content Security Policy
CSP headers tell the browser exactly which resources are allowed to load. They are the single most effective defense against XSS. A strict CSP can block 90%+ of common injection attacks.
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`;The nonce-based approach is ideal for Next.js because it allows your framework-generated inline scripts while blocking everything else. Each request generates a fresh nonce.
Rate Limiting
API routes without rate limiting are an open invitation for brute-force attacks. Whether it's a login endpoint or a data-fetching API, you need to throttle excessive requests.
const rateLimit = new Map<string, { count: number; last: number }>();
export function checkRateLimit(
ip: string,
limit = 10,
windowMs = 60_000
): boolean {
const now = Date.now();
const entry = rateLimit.get(ip);
if (!entry || now - entry.last > windowMs) {
rateLimit.set(ip, { count: 1, last: now });
return true;
}
if (entry.count >= limit) return false;
entry.count++;
return true;
}For production, use a Redis-backed solution like @upstash/ratelimit instead of an in-memory map. In-memory stores don't work across serverless function instances.
Environment Variables
Next.js exposes any variable prefixed with NEXT_PUBLIC_ to the client bundle. This means anyone can read those values by inspecting your JavaScript. Never put API secrets, database URLs, or signing keys in NEXT_PUBLIC_ variables.
- Use NEXT_PUBLIC_ only for truly public values (analytics IDs, public API URLs)
- Keep secrets in server-only env vars accessed through API routes or Server Components
- Validate all env vars at build time with a schema (e.g., zod)
- Rotate secrets regularly and use a vault service for production
Key Takeaways
- Use httpOnly cookies for authentication tokens, not localStorage
- Implement CSP headers with nonce-based script policies
- Rate-limit all API routes, especially authentication endpoints
- Never expose secrets via NEXT_PUBLIC_ environment variables
- Layer your defenses — no single measure is sufficient
Security is an ongoing practice, not a checklist. Audit your dependencies, monitor your logs, and stay current with the OWASP Top 10. The best time to think about security is before you write the first line of code.