API Security Best Practices for Next.js Projects
19 May 2026 · by Yunmin Shin
Why API Security Deserves Specific Attention
A Next.js application exposes API surface area through Route Handlers, Server Actions, and any external APIs it proxies. Each of these is a potential attack vector. Unlike frontend security vulnerabilities that affect individual users, API vulnerabilities can expose your entire database, exhaust your AI API credits, or allow unauthorized data manipulation at scale.
The good news: securing Next.js APIs follows a small set of consistent patterns that are not complicated to implement.
How Do You Authenticate API Requests?
Every API route that accesses user-specific or sensitive data must verify the caller's identity. The standard approach with Supabase:
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
Always use getUser() rather than getSession() for API routes. getUser() validates the JWT with Supabase's servers, while getSession() only decodes it locally — a forged token would pass getSession() but fail getUser().
For server-to-server API calls (from external services to your Next.js app), use a pre-shared secret token. Verify it using a constant-time comparison function (crypto.timingSafeEqual) to prevent timing attacks.
How Do You Validate and Sanitize Input?
Validate all input at the API boundary using Zod before processing it:
const schema = z.object({
amount: z.number().int().positive().max(10_000_000),
recipientId: z.string().uuid(),
note: z.string().max(200).optional(),
});
const result = schema.safeParse(await request.json());
if (!result.success) {
return Response.json({ error: "Invalid input" }, { status: 400 });
}
Reject requests with unexpected fields rather than ignoring them. Use z.object().strict() to fail on extra properties.
How Do You Configure CORS?
Cross-Origin Resource Sharing (CORS) controls which domains can call your API from the browser. In Next.js, set CORS headers in your Route Handler or in middleware.ts:
const allowedOrigins = ["https://yourapp.com", "https://admin.yourapp.com"];
const origin = request.headers.get("origin") ?? "";
if (allowedOrigins.includes(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
}
Never use Access-Control-Allow-Origin: * for authenticated API routes. Wildcard CORS combined with cookies can enable cross-site request forgery.
How Do You Prevent Abuse with Rate Limiting?
Implement per-endpoint rate limits using Upstash Redis. Apply stricter limits to expensive operations:
- Authentication endpoints: 5 attempts per 15 minutes per IP
- AI-powered endpoints: 10 requests per minute per user
- General API routes: 60 requests per minute per user
Return a Retry-After header with 429 responses so well-behaved clients know when to retry.
What Secrets Management Practices Are Essential?
- Store all secrets in environment variables, never in code
- Use separate API keys for development and production
- Rotate secrets immediately if they are accidentally exposed (committed, logged, shared in a screenshot)
- Set up secret scanning in your GitHub repository (GitHub's push protection feature blocks commits containing known secret formats)
- Use minimum-necessary permissions for each API key — a database user used by the web app should not have
DROP TABLEpermissions
Ready to Build Something Fast?
Get a free quote on LINE. We reply within 24 hours.
Ready to build something fast and scalable?
Get a free project quote on LINE. We reply within 24 hours.
무료 견적 on LINE