--- title: "Route Handler (REST API)" status: active audience: [ai-agent, contributor] language: nl last_updated: 2026-05-03 when_to_read: "When writing a new Next.js route handler (GET/POST/PATCH/DELETE)." --- # Patroon: Route Handler (REST API) Alle endpoints vereisen: `Authorization: Bearer ` ## lib/api-auth.ts ```ts import { createHash } from 'crypto' import { prisma } from '@/lib/prisma' export async function authenticateApiRequest(request: Request) { const authHeader = request.headers.get('Authorization') if (!authHeader?.startsWith('Bearer ')) { return { error: 'Unauthorized', status: 401 } } const token = authHeader.slice(7) const tokenHash = createHash('sha256').update(token).digest('hex') const apiToken = await prisma.apiToken.findUnique({ where: { token_hash: tokenHash }, include: { user: true }, }) if (!apiToken || apiToken.revoked_at) { return { error: 'Unauthorized', status: 401 } } return { userId: apiToken.user_id, isDemo: apiToken.user.is_demo } } ``` ## Route Handler ```ts // app/api/products/[id]/next-story/route.ts import { authenticateApiRequest } from '@/lib/api-auth' import { prisma } from '@/lib/prisma' import { productAccessFilter } from '@/lib/product-access' export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const auth = await authenticateApiRequest(request) if ('error' in auth) { return Response.json({ error: auth.error }, { status: auth.status }) } const { id } = await params const sprint = await prisma.sprint.findFirst({ where: { product_id: id, status: 'ACTIVE', product: productAccessFilter(auth.userId) }, }) if (!sprint) { return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 }) } const story = await prisma.story.findFirst({ where: { sprint_id: sprint.id, status: 'IN_SPRINT' }, orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], include: { tasks: { orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }] } }, }) if (!story) { return Response.json({ error: 'Geen open stories in de Sprint' }, { status: 404 }) } return Response.json(story) } ``` ## POST /api/stories/:id/log — body schema ```json { "type": "IMPLEMENTATION_PLAN", "content": "string" } { "type": "TEST_RESULT", "content": "string", "status": "PASSED" | "FAILED" } { "type": "COMMIT", "content": "string", "commit_hash": "string", "commit_message": "string" } ``` ## Alle endpoints | Methode | Endpoint | Doel | |---|---|---| | GET | `/api/products` | Actieve producten ophalen | | GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story | | GET | `/api/sprints/:id/tasks?limit=10` | Eerste N taken van de Sprint | | PATCH | `/api/stories/:id/tasks/reorder` | Taakvolgorde aanpassen | | POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen | | PATCH | `/api/tasks/:id` | Taakstatus bijwerken | | POST | `/api/todos` | Todo aanmaken | ## Security-invarianten - Elk endpoint start met `authenticateApiRequest`. - Schrijf-endpoints geven `403` voor demo-tokens. - Product-scoped reads en writes gebruiken `productAccessFilter(auth.userId)`, zodat eigenaar en gekoppeld teamlid hetzelfde toegangsmodel volgen. - Endpoints die geordende ID-lijsten ontvangen valideren dat elke ID bij de parent-resource hoort voordat er wordt geupdated. - JSON bodies worden met Zod gevalideerd; TypeScript types zijn geen runtime-beveiliging.