| title |
status |
audience |
language |
last_updated |
when_to_read |
| Route Handler (REST API) |
active |
|
nl |
2026-05-03 |
When writing a new Next.js route handler (GET/POST/PATCH/DELETE). |
Patroon: Route Handler (REST API)
Alle endpoints vereisen: Authorization: Bearer <token>
lib/api-auth.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
// 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
{ "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.