This episode covers securing route handlers and request validation with Zod, CORS and CSRF with secure headers, handling sensitive data and secrets, and rate limiting and API abuse prevention.

The API is an application's main attack surface. Endpoints that accept public input — without validation, without limits, without security headers — are an open invitation to abuse.
Episode 13 covers securing route handlers and request validation with Zod, CORS configuration and CSRF protection with secure headers, handling sensitive data and secrets, and rate limiting and abuse prevention.
Rule number one: never trust input. Every body, query, and header from a request must be validated before being used. Combine Zod with a route handler:
import { NextResponse } from "next/server"
import { z } from "zod"
const schema = z.object({
email: z.string().email(),
nama: z.string().min(3).max(100),
})
export async function POST(request) {
const body = await request.json()
const parsed = schema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: "Data tidak valid" },
{ status: 400 }
)
}
const data = parsed.data
return NextResponse.json({ ok: true, data }, { status: 201 })
}schema.safeParse(body) validates the entire body. Returning status 400 when validation fails prevents dirty data from reaching application logic.
Apply limits to input: string length, the number of items in an array, and maximum values for numbers. Server validation must also filter dangerous input like SQL injection — always use prepared statements or an ORM when interacting with a database, never build queries from raw strings.
CORS (Cross-Origin Resource Sharing) determines which domains may call your API. In a route handler, set the headers explicitly:
export async function GET() {
const headers = new Headers()
headers.set("Access-Control-Allow-Origin", "https://app.example.com")
headers.set("Access-Control-Allow-Methods", "GET, POST")
return NextResponse.json({ data: [] }, { headers })
}The Access-Control-Allow-Origin header above restricts calls to the app.example.com domain. Don't use a wildcard for APIs that carry user data.
CSRF attacks applications that implicitly trust cookies. Next.js Server Actions are protected by default, but for route handlers that use session cookies, make sure a CSRF token is present. Also complete it with secure headers:
X-Frame-Options: DENY prevents clickjacking.X-Content-Type-Options: nosniff prevents content type sniffing.Strict-Transport-Security enforces HTTPS connections.Content-Security-Policy restricts script and style sources.The headers above can be set automatically through the headers option in next.config.mjs, applying them to every response.
Secrets like API keys, database tokens, and signing keys should only exist in the server environment. Never: put them in code, put them in NEXT_PUBLIC_ variables, or put them in Git history. Use the .env.local file (which is in .gitignore) and inject real values from the deployment platform in production.
Sensitive data like passwords must be hashed — never store plaintext passwords. Use a library like bcrypt with a salt. The principle of data minimization: store only the data truly needed, and delete or anonymize old data. The less sensitive data stored, the smaller the impact when a breach occurs.
Public APIs must be rate-limited to prevent brute force and spam. On the edge, use distributed storage like Upstash Redis:
import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"),
})
export async function POST(request) {
const ip = request.headers.get("x-forwarded-for")
const result = await ratelimit.limit(`api-${ip}`)
if (!result.success) {
return new Response("Terlalu banyak permintaan", { status: 429 })
}
return new Response("OK")
}ratelimit.limit("api-" + ip) returns a success flag indicating whether the request exceeds the quota of 10 requests per minute. Status 429 indicates Too Many Requests.
Here's what to take away:
In the next episode, episode 14, we'll discuss networking performance and caching — caching strategies with Cache-Control and ISR, edge caching and CDN integration, optimizing data fetching to reduce API latency, and prefetching with resource scheduling. Your application will feel much faster.