Skip to content

API Patterns

Reusable conventions for every REST API built within GE. These are not suggestions. They are the patterns Koen enforces in CI and Jaap validates as SSOT.


Resource Naming

Rules

  • Use plural nouns for collections: /projects, /users, /invoices
  • Use kebab-case for multi-word resources: /work-packages, /session-learnings
  • Use path nesting for ownership: /projects/{projectId}/tasks
  • Maximum nesting depth: 2 levels. Beyond that, use top-level resources with query filters
  • No verbs in paths. Use HTTP methods to express actions
  • No trailing slashes. /projects not /projects/

Examples

GET    /projects                     → List projects
POST   /projects                     → Create project
GET    /projects/{projectId}         → Get single project
PATCH  /projects/{projectId}         → Partial update
DELETE /projects/{projectId}         → Delete project
GET    /projects/{projectId}/tasks   → List tasks in project

Anti-patterns

GET  /getProjects          → verb in path
POST /projects/create      → verb in path
GET  /project              → singular collection name
GET  /Projects             → wrong case
GET  /projects/1/tasks/2/comments/3/reactions  → too deep

HTTP Methods

Method Semantics Idempotent Request Body Success Code
GET Read resource(s) Yes No 200
POST Create resource No Yes 201
PUT Full replace Yes Yes 200
PATCH Partial update No* Yes 200
DELETE Remove resource Yes No 204
HEAD Metadata only Yes No 200
OPTIONS CORS preflight Yes No 204

*PATCH can be made idempotent with JSON Merge Patch (RFC 7396). GE prefers JSON Merge Patch over JSON Patch (RFC 6902) for simplicity.


Status Codes

Success

Code When
200 OK GET, PUT, PATCH success
201 Created POST created a resource. Include Location header
204 No Content DELETE success, or PUT/PATCH with no response body

Client Error

Code When
400 Bad Request Validation failure, malformed body
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but not authorized
404 Not Found Resource does not exist
409 Conflict Business rule violation, duplicate, state conflict
422 Unprocessable Entity Semantically invalid (valid JSON, wrong values)
429 Too Many Requests Rate limit exceeded

Server Error

Code When
500 Internal Server Error Unhandled exception
502 Bad Gateway Upstream service failure
503 Service Unavailable Maintenance or overload
504 Gateway Timeout Upstream timeout

Rule: Never return 200 with an error body. Use the status code.


API Versioning

Strategy: URL path versioning

/api/v1/projects
/api/v2/projects

Rules

  • Major version in URL path — breaking changes only
  • Minor/patch changes are backward-compatible, no version bump
  • Support at most 2 major versions simultaneously
  • Deprecation header on the old version: Deprecation: true with Sunset header indicating removal date
  • Changelog in the OpenAPI description for each version

What constitutes a breaking change

  • Removing a field from a response
  • Changing a field type
  • Renaming a field
  • Adding a required field to a request
  • Changing the meaning of a status code
  • Removing an endpoint

What is NOT a breaking change

  • Adding an optional field to a request
  • Adding a field to a response
  • Adding a new endpoint
  • Adding a new query parameter with a default value

Pagination

Strategy: Cursor-based pagination

GE uses cursor-based pagination for all list endpoints. Offset-based pagination breaks when data changes between pages and performs poorly at scale (OFFSET requires scanning).

Request parameters

Parameter Type Default Description
cursor string (none) Opaque cursor from previous response
limit integer 20 Items per page (max: 100)

Response format

{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTAwfQ==",
    "has_more": true,
    "total_count": 342
  }
}

Rules

  • next_cursor is opaque — clients must not parse or construct it
  • total_count is optional (expensive on large tables, omit if > 10k rows)
  • First page request omits cursor
  • Empty page returns data: [] with has_more: false
  • Cursor encodes the sort key, not the offset

Filtering and Sorting

Filtering

Use query parameters with field names:

GET /projects?status=active&team=alfa
GET /tasks?created_after=2026-01-01&priority=high

Rules

  • Simple equality: ?field=value
  • Multiple values (OR): ?status=active,completed (comma-separated)
  • Range: ?created_after=2026-01-01&created_before=2026-02-01
  • Negation: ?status!=archived (only when necessary)
  • Search: ?q=search+term for full-text search

Do not invent a query language. If filtering becomes complex, the endpoint probably needs to be redesigned.

Sorting

GET /projects?sort=created_at:desc
GET /tasks?sort=priority:asc,created_at:desc
  • Format: field:direction (asc/desc)
  • Multiple fields separated by commas
  • Default sort must be documented in the spec

Error Format: RFC 9457 Problem Details

Every error response uses the RFC 9457 (successor to RFC 7807) format. This is not optional.

Structure

{
  "type": "https://api.growingeurope.io/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The field 'email' must be a valid email address.",
  "instance": "/projects/abc-123/members",
  "errors": [
    {
      "field": "email",
      "message": "Must be a valid email address",
      "code": "invalid_format"
    }
  ]
}

Fields

Field Required Description
type Yes URI identifying the error type
title Yes Human-readable summary
status Yes HTTP status code (must match response)
detail Yes Specific explanation for this occurrence
instance No URI of the request that caused the error
errors No Array of field-level errors (validation)

Rules

  • type URI should resolve to documentation explaining the error
  • title is generic (same for all instances of this type)
  • detail is specific (unique to this occurrence)
  • Content-Type: application/problem+json
  • Extensions are allowed — add fields as needed, clients ignore unknowns

Rate Limiting

Response headers

Every response must include rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1711234567

429 response

{
  "type": "https://api.growingeurope.io/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded 100 requests per minute. Try again in 18 seconds.",
  "retry_after": 18
}

Include Retry-After header (seconds) on 429 responses.

Tiers

Tier Limit Scope
Public 60 req/min Per IP
Authenticated 300 req/min Per API key
Internal (agent) 1000 req/min Per agent identity

Authentication

JWT + API Keys

GE uses two authentication mechanisms:

JWT (Bearer tokens) for user sessions:

Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
  • Short-lived (15 min)
  • Refresh via httpOnly cookie
  • ES256 algorithm (not RS256 — smaller tokens, faster verification)

API Keys for service-to-service and agent communication:

Authorization: Bearer ge_live_abc123...
  • Prefixed: ge_live_ (production) or ge_test_ (staging)
  • Stored hashed in DB (bcrypt)
  • Rotatable without downtime
  • Scoped to specific permissions

Rules

  • Never send tokens in query parameters (logged in access logs)
  • Never send tokens in URL fragments
  • Always validate token expiry
  • Always validate token audience (aud claim)
  • Revocation list checked on every request

File Upload Patterns

Small files (< 10 MB)

Multipart form data to the resource endpoint:

POST /projects/{projectId}/attachments
Content-Type: multipart/form-data

Large files (> 10 MB)

Pre-signed URL pattern:

1. POST /uploads → returns { upload_url, upload_id }
2. PUT {upload_url} → client uploads directly to storage
3. POST /projects/{projectId}/attachments
   Body: { upload_id: "..." }

Rules

  • Validate MIME type server-side (never trust Content-Type header)
  • Enforce file size limits per endpoint
  • Scan for malware before making files accessible
  • Return upload progress via Content-Range on resumable uploads

Webhook Design

Registration

POST /webhooks
{
  "url": "https://client.example.com/hooks",
  "events": ["project.created", "task.completed"],
  "secret": "whsec_abc123..."
}

Delivery

POST https://client.example.com/hooks
X-Webhook-Signature: sha256=abc123...
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1711234567

{
  "id": "evt_abc123",
  "type": "project.created",
  "created_at": "2026-03-26T12:00:00Z",
  "data": { ... }
}

Rules

  • Sign payloads with HMAC-SHA256 using the webhook secret
  • Include event ID for idempotency
  • Retry with exponential backoff (3 attempts: 1s, 30s, 300s)
  • Disable webhook after 10 consecutive failures
  • Log all delivery attempts
  • Timestamp to prevent replay attacks (reject if > 5 min old)

HATEOAS

Hypermedia as the Engine of Application State. Use it selectively.

When useful

  • Public APIs where discoverability matters
  • APIs consumed by generic clients (not code-generated)
  • Navigation between related resources

When to skip

  • Internal agent-to-agent APIs (agents know the spec)
  • High-throughput endpoints (link overhead adds up)

Format (when used)

{
  "data": { "id": "proj_123", "name": "Client Site" },
  "links": {
    "self": "/api/v1/projects/proj_123",
    "tasks": "/api/v1/projects/proj_123/tasks",
    "team": "/api/v1/teams/alfa"
  }
}

Response Envelope

All GE APIs use a consistent response envelope:

Single resource

{
  "data": { ... }
}

Collection

{
  "data": [...],
  "pagination": { ... }
}

Error

{
  "type": "...",
  "title": "...",
  "status": 422,
  "detail": "..."
}

Errors do NOT use the data wrapper. The Problem Details format is the response body directly.


Ownership

Role Agent Responsibility
Backend Team Lead (Alfa) Urszula Pattern enforcement, design review
Backend Team Lead (Bravo) Maxim Pattern enforcement, design review
Code Quality Koen Automated pattern validation in CI
SSOT Enforcer Jaap Spec-implementation consistency
Formal Specification Anna API contracts in functional specs