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.
/projectsnot/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¶
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: truewithSunsetheader 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_cursoris opaque — clients must not parse or construct ittotal_countis optional (expensive on large tables, omit if > 10k rows)- First page request omits
cursor - Empty page returns
data: []withhas_more: false - Cursor encodes the sort key, not the offset
Filtering and Sorting¶
Filtering¶
Use query parameters with field names:
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+termfor full-text search
Do not invent a query language. If filtering becomes complex, the endpoint probably needs to be redesigned.
Sorting¶
- 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¶
typeURI should resolve to documentation explaining the errortitleis generic (same for all instances of this type)detailis 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:
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:
- 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:
- Prefixed:
ge_live_(production) orge_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 (
audclaim) - Revocation list checked on every request
File Upload Patterns¶
Small files (< 10 MB)¶
Multipart form data to the resource endpoint:
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-Typeheader) - Enforce file size limits per endpoint
- Scan for malware before making files accessible
- Return upload progress via
Content-Rangeon 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¶
Collection¶
Error¶
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 |