Next.js
I. Next.js Fundamentals
1. Introduction to Next.js
2. Next.js vs React
3. Project Setup
4. Folder Structure
5. Pages and Routing
6. Link and Navigation
7. Static Assets
8. CSS and Styling
II. Routing, Data & Rendering
9. Dynamic Routing
10. API Routes
11. Data Fetching Basics
12. getStaticProps
13. getServerSideProps
14. Incremental Static Regeneration
15. Rendering Strategies
16. Image Optimization
III. Advanced Next.js & Performance
17. Middleware
18. Authentication Basics
19. Authorization
20. Environment Variables
21. Performance Optimization
22. SEO with Next.js
23. Internationalization
24. Error Handling
IV. Projects, Deployment & Best Practices
25. Next.js Best Practices
26. Folder and Code Organization
27. Testing Next.js Apps
28. Security in Next.js
29. Next.js Interview Questions
30. Mini Project – Blog
31. Mini Project – Dashboard
32. Mini Project – Ecommerce
33. Mini Project – API Integration
34. Next.js Case Study
35. Real-World Use Cases
36. Project Planning
37. Final Project
38. Deployment with Vercel
39. Course Review
40. Career Roadmap
Lesson 28
Security in Next.js
Secure your NewsWave application against common vulnerabilities and implement security best practices throughout your Next.js stack.
Security isn't just about passwords and locks. Think of it like building a house — you need secure foundations, proper doors, good locks, and smart systems that keep the bad actors out while letting legitimate visitors in comfortably. Next.js applications face unique security challenges because they run code in multiple environments. Your components render on the server, your API routes handle sensitive data, and your client-side code runs in browsers you don't control. Each environment needs its own security strategy. Unlike traditional client-only React apps, Next.js gives you server-side powers. With great power comes great responsibility — server-side code can access databases, file systems, and environment variables that should never leak to the browser. One misplacedconsole.log in the wrong place could expose API keys to every visitor.
Environment Variables Security
Environment variables hold your secrets — database URLs, API keys, JWT secrets. Next.js has a built-in system that prevents most accidents, but you need to understand how it works. Next.js treats environment variables differently based on their prefix. Variables starting withNEXT_PUBLIC_ become available in the browser. Everything else stays server-only. This simple naming convention prevents most environment variable leaks.
But accidents happen. Developers sometimes expose server-only variables by destructuring process.env in client components or logging sensitive data during development. The NewsWave team needs bulletproof environment variable handling.
// .env.local - Server-only secrets
DATABASE_URL="postgresql://user:pass@localhost:5432/newswave"
JWT_SECRET="your-super-secret-jwt-key-here"
STRIPE_SECRET_KEY="sk_test_..."
// Public variables available in browser
NEXT_PUBLIC_API_URL="https://api.newswave.com"
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXXXXX"Terminal
$ echo "NODE_ENV=production" >> .env.local
Environment variables loaded
✓ Server secrets protected from browser
What just happened?
Environment variables without NEXT_PUBLIC_ stay on the server. The build process never includes them in client bundles. Try this: Add a database URL to your environment file and try to access it in a client component — Next.js will return undefined.
process.env access returns strings or undefined, leading to runtime errors when your app expects specific formats.
// lib/env.js - Validated environment config
export const env = {
// Server-only variables with validation
DATABASE_URL: process.env.DATABASE_URL || (() => {
throw new Error('DATABASE_URL is required')
})(),
JWT_SECRET: process.env.JWT_SECRET || (() => {
throw new Error('JWT_SECRET is required')
})()
}Terminal
$ npm run build
Validating environment variables...
✓ All required variables present
What just happened?
Environment validation runs at import time, catching missing variables before they cause runtime errors. Build processes fail fast instead of deploying broken configurations. Try this: Remove a required environment variable and watch your build fail with a clear error message.
Content Security Policy
Content Security Policy (CSP) acts like a bouncer for your website. It tells browsers which scripts, styles, and resources are allowed to run. Without CSP, any injected script can run with full access to your user's session and data. Modern web applications load resources from multiple sources — CDNs for fonts, analytics scripts, payment processors. CSP lets you create an allowlist of trusted sources while blocking everything else. Think of it as airport security for your web app. Next.js applications need carefully crafted CSP headers because they use inline styles for CSS-in-JS libraries and dynamic script loading for code splitting. Too restrictive and your app breaks. Too permissive and you're vulnerable to XSS attacks.// next.config.js - CSP headers configuration
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' *.vercel-analytics.com;
style-src 'self' 'unsafe-inline' fonts.googleapis.com;
font-src 'self' fonts.gstatic.com;
img-src 'self' data: https:;
`.replace(/\s{2,}/g, ' ').trim()
}
]Terminal
$ npm run dev
CSP headers applied to all routes
✓ Browser enforcing security policy
What just happened?
CSP headers instruct browsers to only load resources from approved sources. Malicious scripts from untrusted domains get blocked automatically. Try this: Open browser dev tools and check the Network tab for CSP violations when loading external resources.
// middleware.js - CSP with nonces
import { NextResponse } from 'next/server'
import crypto from 'crypto'
export function middleware(request) {
const nonce = crypto.randomBytes(16).toString('base64') // Generate unique nonce
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
`.replace(/\s{2,}/g, ' ').trim()
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', csp)
return response
}Terminal
$ curl -I http://localhost:3000
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'
✓ Unique nonce generated per request
What just happened?
Each request generates a unique nonce that gets embedded in both the CSP header and inline scripts. Attackers can't guess the nonce, so injected scripts fail to execute. Try this: View page source and notice how the nonce appears in both the CSP header and script tags.
API Route Security
API routes handle sensitive operations like user authentication, payment processing, and data mutations. They're prime targets for attackers because they often have elevated privileges and direct database access. Rate limiting prevents abuse by limiting how many requests a client can make within a time window. Without rate limiting, attackers can brute force passwords, overwhelm your database, or create denial of service conditions for legitimate users. Input validation ensures that API routes receive expected data types and formats. Malicious clients can send unexpected data that crashes your server, corrupts your database, or exposes sensitive information through error messages.// lib/rate-limit.js - Simple rate limiting
const rateLimits = new Map()
export function rateLimit(ip, limit = 100, window = 3600000) { // 100 requests per hour
const now = Date.now()
const windowStart = now - window
if (!rateLimits.has(ip)) {
rateLimits.set(ip, [])
}
const requests = rateLimits.get(ip).filter(time => time > windowStart)
if (requests.length >= limit) {
return false // Rate limited
}
requests.push(now)
rateLimits.set(ip, requests)
return true // Request allowed
}Terminal
$ node -e "console.log('Rate limiter loaded')"
Rate limiting active on API routes
✓ Preventing abuse and brute force attacks
What just happened?
Rate limiting tracks request timestamps per IP address and blocks excessive requests within the time window. Memory-based storage works for single instances but needs Redis for production scaling. Try this: Make rapid requests to your API and watch rate limiting kick in after the limit.
// app/api/articles/route.js - Validated API route
import { z } from 'zod'
import { rateLimit } from '@/lib/rate-limit'
const createArticleSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
category: z.enum(['tech', 'world', 'science', 'business', 'sports']),
tags: z.array(z.string()).max(10).optional()
})
export async function POST(request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
if (!rateLimit(ip, 10)) { // 10 posts per hour
return Response.json({ error: 'Rate limited' }, { status: 429 })
}
try {
const body = await request.json()
const validatedData = createArticleSchema.parse(body)
// Safe to use validatedData - it matches our schema
console.log('Creating article:', validatedData.title)
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: 'Invalid data' }, { status: 400 })
}
}Terminal
$ curl -X POST -d '{"title":"Test"}' http://localhost:3000/api/articles
{"error":"Invalid data"}
✓ Malformed request rejected safely
What just happened?
Rate limiting and input validation work together to protect API routes. Invalid data gets rejected before reaching business logic, preventing crashes and data corruption. Try this: Send various malformed payloads and observe how validation catches different error types.
Authentication Security
Authentication vulnerabilities are among the most dangerous because they give attackers access to user accounts and sensitive data. Weak session management, predictable tokens, and insecure password handling create opportunities for account takeovers. JSON Web Tokens (JWTs) need careful handling in Next.js applications. Server-side routes can verify tokens securely, but client-side code should never access raw JWT secrets. Token storage location also matters — localStorage persists across browser sessions but remains accessible to XSS attacks. Session management requires balancing security with user experience. Short-lived tokens improve security but require frequent renewal. Long-lived tokens are convenient but increase risk if compromised. Refresh token rotation provides a middle ground.// lib/auth.js - Secure JWT handling
import jwt from 'jsonwebtoken'
import { cookies } from 'next/headers'
export function generateTokens(userId) {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short-lived access token
)
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_SECRET,
{ expiresIn: '7d' } // Longer-lived refresh token
)
return { accessToken, refreshToken }
}Terminal
$ npm install jsonwebtoken bcrypt
Adding secure authentication libraries
✓ JWT and password hashing ready
What just happened?
Short-lived access tokens limit exposure if compromised. Refresh tokens allow seamless renewal without frequent login prompts. The dual-token approach balances security and usability. Try this: Generate tokens and check their expiration times using a JWT decoder.
// app/api/login/route.js - Secure cookie setting
export async function POST(request) {
const { email, password } = await request.json()
// Verify credentials (implementation details omitted)
const user = await verifyCredentials(email, password)
if (!user) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
const { accessToken, refreshToken } = generateTokens(user.id)
const response = Response.json({ success: true })
// Secure cookie configuration
response.cookies.set('accessToken', accessToken, {
httpOnly: true, // Prevents XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60, // 15 minutes
path: '/'
})
response.cookies.set('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60, // 7 days
path: '/'
})
return response
}Terminal
$ curl -X POST -d '{"email":"test@news.com","password":"pass"}' http://localhost:3000/api/login
Set-Cookie: accessToken=...; HttpOnly; Secure; SameSite=Strict
✓ Secure authentication cookies set
What just happened?
Secure cookie flags create multiple layers of protection. HttpOnly blocks XSS, Secure requires encrypted connections, and SameSite prevents cross-site attacks. Browser developer tools show these flags in the cookies tab. Try this: Login and inspect cookies in browser dev tools to see security flags applied.
HTTPS and Security Headers
HTTPS encrypts all communication between browsers and your server. Without HTTPS, sensitive data travels in plain text where anyone monitoring network traffic can read passwords, session tokens, and private information. Next.js applications deployed on Vercel get HTTPS automatically. But local development often uses HTTP, creating a disconnect between development and production environments. This mismatch can hide security issues until they reach production. Security headers provide additional protection beyond HTTPS. They tell browsers to enforce security policies, prevent certain types of attacks, and handle sensitive operations more carefully. Modern applications should implement a complete set of security headers.// next.config.js - Complete security headers
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on' // Allow DNS prefetching for performance
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload' // Force HTTPS
},
{
key: 'X-Frame-Options',
value: 'DENY' // Prevent embedding in iframes
},
{
key: 'X-Content-Type-Options',
value: 'nosniff' // Prevent MIME type sniffing
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin' // Control referrer information
}
]
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders
}
]
}
}Terminal
$ npm run build
Security headers configured for all routes
✓ Browser security policies enforced
What just happened?
Security headers provide defense against different attack vectors. HSTS forces HTTPS, X-Frame-Options prevents clickjacking, and X-Content-Type-Options stops MIME sniffing attacks. Try this: Check security headers in browser dev tools Network tab when loading any page.
// package.json - HTTPS development server
{
"scripts": {
"dev": "next dev",
"dev:https": "next dev --experimental-https",
"build": "next build",
"start": "next start"
}
}Terminal
$ npm run dev:https
Local development server running on https://localhost:3000
✓ HTTPS enabled for development
What just happened?
Local HTTPS development matches production security requirements. Browser warnings about self-signed certificates are normal for development but should never appear in production. Try this: Run HTTPS development and notice how security features like secure cookies now work properly.
Data Sanitization and XSS Prevention
Cross-site scripting (XSS) attacks inject malicious scripts into your application that run with your users' privileges. React provides some XSS protection by default, but developers can accidentally create vulnerabilities through unsafe patterns. React escapes values automatically when rendered as text content. ButdangerouslySetInnerHTML bypasses this protection, creating XSS risks if used with unsanitized content. URL handling, dynamic imports, and server-side rendering also create potential attack vectors.
Content sanitization removes or encodes dangerous HTML, JavaScript, and other executable content from user input. Different types of content require different sanitization approaches — HTML content needs different handling than URLs or file names.
// lib/sanitize.js - Content sanitization utilities
import DOMPurify from 'dompurify'
import { JSDOM } from 'jsdom'
const window = new JSDOM('').window
const purify = DOMPurify(window)
export function sanitizeHTML(html) {
return purify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false
})
}
export function sanitizeURL(url) {
try {
const parsed = new URL(url)
// Only allow http/https protocols
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null
}
return parsed.href
} catch {
return null
}
}Terminal
$ npm install dompurify jsdom
Installing content sanitization libraries
✓ XSS protection tools ready
What just happened?
DOMPurify removes dangerous HTML elements and attributes while preserving safe formatting. URL sanitization prevents javascript: and data: protocol attacks. Try this: Test sanitization by passing malicious HTML like <script>alert('xss')</script> to see it get cleaned.
dangerouslySetInnerHTML. Never trust user content, even from authenticated users or internal sources.
// components/SafeHTML.js - Secure HTML rendering component
import { sanitizeHTML } from '@/lib/sanitize'
export default function SafeHTML({ content, className = '' }) {
const cleanHTML = sanitizeHTML(content)
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: cleanHTML }}
/>
)
}
// Usage in article display
export default function ArticleContent({ article }) {
return (
<div>
<h1>{article.title}</h1> {/* Safe - React escapes automatically */}
<SafeHTML content={article.body} className="prose" /> {/* Sanitized HTML */}
</div>
)
}localhost:3000 — NewsWave
What just happened?
Content sanitization removes dangerous script tags while preserving safe HTML formatting. The SafeHTML component provides a reusable wrapper for secure HTML rendering. Try this: Inspect the rendered HTML to see how malicious scripts were stripped while keeping safe formatting intact.
Security Best Practices Summary
Environment variables should be validated and prefixed correctly. API routes need rate limiting and input validation. Authentication requires secure tokens and HTTP-only cookies. Content must be sanitized before rendering as HTML. Security headers provide defense in depth against multiple attack vectors.
Quiz
1. The NewsWave team needs to add a Google Analytics ID to their app. How should environment variables be handled in Next.js?
2. What cookie configuration provides the best security for NewsWave's authentication tokens?
3. How should NewsWave safely render user-generated HTML content in article comments?
Up Next: Next.js Interview Questions
Master common Next.js interview questions and technical challenges to land your next role with confidence.