Scrum4Me/docs/patterns/route-handler.md
Janpeter Visser f7464db837
docs: sync data-model, glossary en specs met huidig schema (#164)
Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63
en M12. Belangrijkste fixes:

- data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten
  (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob,
  SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing,
  PushSubscription, ModelPrice, ProductMember), nieuwe enums
  (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes
  (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd.
- glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl.
  FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/
  verify_result toegevoegd.
- project-structure.md: app/(app)/todos vervangen door
  ideas/insights/jobs/manual/admin/solo; api-tree volledig.
- overview.md: "geen realtime in v1" en Docker-rationale herschreven —
  Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in
  Docker-deploy-flow.
- functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint,
  F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst,
  navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt.
- README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health
  toegevoegd, kort over realtime/auth-pair/internal/cron.
- patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen
  door Idea/OPEN; create_todo MCP-tool note over verwijdering.

Linkcheck groen (105 files), INDEX hergegenereerd (98 docs).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:16:44 +02:00

3.7 KiB

title status audience language last_updated when_to_read
Route Handler (REST API) active
ai-agent
contributor
nl 2026-05-08 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: 'OPEN', 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/health Liveness; ?db=1 voor DB-ping (geen auth)
GET /api/products Actieve producten ophalen
GET /api/products/:id/next-story Hoogst geprioriteerde open story
GET /api/products/:id/claude-context Bundled MCP-context
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 / implementation_plan bijwerken
GET / POST /api/ideas, GET / PATCH /api/ideas/:id Idea CRUD (vervangt voormalig /api/todos)
GET /api/jobs/:id/sub-tasks sprint_task_executions van een SPRINT_IMPLEMENTATION-job

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.