Skip to main content

Server Auth Actions

Source: .agents/references/coding-standard/vercel-react-best-practices/rules/server-auth-actions.md

Metadata

  • title: Authenticate API Routes Like Public Endpoints
  • impact: CRITICAL
  • impactDescription: prevents unauthorized access to server mutations
  • tags: server, api-routes, pages-api, authentication, security, authorization

Content

This repo: Pages Router only — authenticate src/pages/api/ handlers. Server Actions ("use server") are not used; skip App Router Server Action examples unless migrating.

Authenticate API Routes Like Public Endpoints

Impact: CRITICAL (prevents unauthorized access to server mutations)

pages/api routes are public HTTP endpoints. Always verify authentication and authorization inside each handler — do not rely only on client-side checks or page-level guards, since the route can be called directly.

Incorrect (no authentication check):

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { userId } = req.body
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } })
return res.status(200).json({ success: true })
}

Correct (authentication inside the handler):

import type { NextApiRequest, NextApiResponse } from 'next'
import { verifySession } from '@/lib/auth'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await verifySession(req)

if (!session) {
return res.status(401).json({ error: 'Must be logged in' })
}

const { userId } = req.body

if (session.user.role !== 'admin' && session.user.id !== userId) {
return res.status(403).json({ error: 'Cannot delete other users' })
}

await db.user.delete({ where: { id: userId } })
return res.status(200).json({ success: true })
}

With input validation:

import type { NextApiRequest, NextApiResponse } from 'next'
import { verifySession } from '@/lib/auth'
import { z } from 'zod'

const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const validated = updateProfileSchema.parse(req.body)

const session = await verifySession(req)
if (!session) {
return res.status(401).json({ error: 'Unauthorized' })
}

if (session.user.id !== validated.userId) {
return res.status(403).json({ error: 'Can only update own profile' })
}

await db.user.update({
where: { id: validated.userId },
data: { name: validated.name, email: validated.email }
})

return res.status(200).json({ success: true })
}

Reference: Next.js Pages API Routes