Back to docs
Recipes

Route handler patterns

Production-tested patterns for App Router route handlers — caching, streaming, auth guards, and error envelopes.

Auth guard wrapper

Wrap every handler in a reusable guard that validates the session before the route body executes. Return 401 with a consistent envelope.

export function withAuth(handler) {
  return async (req) => {
    const session = await validateSession(req)
    if (!session) {
      return Response.json(
        { error: 'unauthorized' },
        { status: 401 }
      )
    }
    return handler(req, session)
  }
}

Streaming response

Use a ReadableStream when the response payload is large or generated incrementally. Set the correct headers so the client can consume chunks as they arrive.

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        controller.enqueue(
          new TextEncoder().encode(`chunk ${i}\n`)
        )
        await new Promise(r => setTimeout(r, 500))
      }
      controller.close()
    }
  })
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain',
      'X-Content-Type-Options': 'nosniff'
    }
  })
}

Cache-control by verb

GET responses are cached at the edge for 60 seconds. Mutations skip the cache entirely and return a no-store directive.

export async function GET() {
  const data = await fetchFromKV()
  return Response.json(data, {
    headers: {
      'Cache-Control':
        'public, s-maxage=60, stale-while-revalidate=30'
    }
  })
}

export async function POST(req: Request) {
  const body = await req.json()
  await writeToKV(body)
  return Response.json({ ok: true }, {
    headers: { 'Cache-Control': 'no-store' }
  })
}

Error envelope

Every route returns the same JSON shape so clients can branch on the error code without parsing status text.

function err(code: string, status: number) {
  return Response.json({ error: code }, { status })
}

// Usage
if (!body.email) return err('missing_email', 400)
if (duplicate)    return err('email_taken', 409)

These patterns power every API route under /api/v1. Copy them freely — they are extracted directly from the Meridian production codebase.