Skip to content

API Security

API routes handle sensitive operations — creating data, processing payments, accessing user information. Securing them is critical.

ThreatDescriptionPrevention
Unauthorized accessUnauthenticated users accessing protected dataAuthentication check
Injection attacksSQL, NoSQL, or command injectionUse ORM, validate input
Rate limiting abuseToo many requests overwhelming the serverRate limiting
IDORAccessing another user’s data by changing an IDOwnership check

Every protected API route should start with an auth check:

app/api/posts/route.ts
export async function POST(request: Request) {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
// Proceed with authenticated logic
}

Always validate and sanitize user input:

import { z } from 'zod'
const createPostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10).max(5000),
})
export async function POST(request: Request) {
const body = await request.json()
const parsed = createPostSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.flatten() },
{ status: 400 }
)
}
// parsed.data is safely typed
}

Prevent users from accessing other users’ data:

export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const post = await db.post.findUnique({ where: { id: params.id } })
if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 })
if (post.authorId !== session.user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
await db.post.delete({ where: { id: params.id } })
return new NextResponse(null, { status: 204 })
}
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
const { success } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
}
// Proceed
}
  • Not validating input — Trusting user input without validation is the most common security vulnerability.
  • Inconsistent auth checks — Checking auth in one route but not another. Apply it consistently.
  • No rate limiting on public endpoints — Public APIs without rate limits can be abused.
  • Validate every input with Zod or similar
  • Check authentication at the start of every protected handler
  • Return appropriate status codes (401, 403, 404, 429)
  • Use rate limiting for public endpoints
  • Never expose internal error details to the client

Secure API routes with authentication checks, input validation, ownership verification, and rate limiting. Validate with Zod, check session at the start of every handler, and never trust user input.