API Design Standards
API Design Standards
Scope: REST API conventions cho 6 microservices + ACL + Gateway
Principle: Consistent, predictable, self-documenting APIs. Consumer-first design.
Standard: RESTful + OpenAPI 3.1 + JSON:API inspired (not full JSON:API)
Source: Architect.md, Business Domain.md, Testing Strategy.md
1. URL Conventions
1.1 Base Pattern
https://api.product.com/{version}/{service}/{resource}
Examples:
https://api.product.com/v1/travel/bookings
https://api.product.com/v1/events/registrations
https://api.product.com/v1/workforce/allocations
https://api.product.com/v1/comms/notifications
https://api.product.com/v1/reports/team-hours
1.2 URL Rules
| Rule | Good | Bad | Why |
|---|---|---|---|
| Nouns, not verbs | /bookings |
/getBookings |
HTTP method = verb |
| Plural resources | /bookings |
/booking |
Collection semantics |
| Kebab-case | /team-hours |
/teamHours, /team_hours |
URL standard |
| Lowercase | /bookings |
/Bookings |
Case sensitivity issues |
| No trailing slash | /bookings |
/bookings/ |
Consistency |
| Max 3 levels deep | /bookings/{id}/passengers |
/bookings/{id}/passengers/{pid}/documents/{did} |
Complexity → use query params or separate endpoint |
1.3 Resource Hierarchy
Travel Service:
GET /v1/travel/bookings List bookings (filtered)
POST /v1/travel/bookings Create booking
GET /v1/travel/bookings/{id} Get booking by ID
PUT /v1/travel/bookings/{id} Update booking
DELETE /v1/travel/bookings/{id} Cancel booking
GET /v1/travel/bookings/{id}/passengers List passengers for booking
POST /v1/travel/bookings/{id}/passengers Add passenger
Event Service:
GET /v1/events List events
POST /v1/events Create event
GET /v1/events/{id} Get event
PUT /v1/events/{id} Update event
POST /v1/events/{id}/registrations Register for event
DELETE /v1/events/{id}/registrations/{rid} Cancel registration
Workforce Service:
GET /v1/workforce/employees List employees
GET /v1/workforce/employees/{id} Get employee
GET /v1/workforce/allocations List allocations
POST /v1/workforce/allocations Create allocation
GET /v1/workforce/teams/{id}/schedule Get team schedule
Comms Service:
POST /v1/comms/notifications Send notification
GET /v1/comms/notifications List notifications (user's)
PUT /v1/comms/notifications/{id}/read Mark as read
GET /v1/comms/templates List templates
Reporting Service:
GET /v1/reports/team-hours Team hours report
GET /v1/reports/bookings-summary Booking summary
GET /v1/reports/event-attendance Event attendance
POST /v1/reports/export Export report (async)
GET /v1/reports/export/{jobId}/status Check export status
2. HTTP Methods & Status Codes
2.1 Methods
| Method | Usage | Idempotent | Safe |
|---|---|---|---|
| GET | Read resource(s) | Yes | Yes |
| POST | Create resource, trigger action | No | No |
| PUT | Full update (replace) | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove resource | Yes | No |
2.2 Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 OK | Request succeeded | GET, PUT, PATCH success |
| 201 Created | Resource created | POST success. Include Location header |
| 204 No Content | Success, no body | DELETE success |
| 400 Bad Request | Client error — validation failed | Missing fields, invalid format |
| 401 Unauthorized | Not authenticated | Missing/invalid JWT |
| 403 Forbidden | Authenticated but not authorized | Valid JWT but insufficient role/permission |
| 404 Not Found | Resource doesn't exist | GET/PUT/DELETE on non-existent ID |
| 409 Conflict | Business rule violation | Duplicate booking, concurrent update |
| 422 Unprocessable Entity | Valid format but business logic rejects | Cancel already-cancelled booking |
| 429 Too Many Requests | Rate limited | Include Retry-After header |
| 500 Internal Server Error | Unexpected server error | Unhandled exception (should be rare) |
| 502 Bad Gateway | Upstream service failure | ACL → legacy monolith down |
| 503 Service Unavailable | Service overloaded | Circuit breaker open |
NEVER use:
- 200 for errors (even with error body) — status code = truth
- 500 for client errors — distinguish 4xx vs 5xx always
- Custom status codes (e.g., 499) — stick to RFC standards
3. Request & Response Format
3.1 Request Standards
// POST /v1/travel/bookings
// Content-Type: application/json
// Authorization: Bearer {jwt}
// X-Correlation-Id: {auto-generated if missing}
// X-Idempotency-Key: {client-generated UUID for POST requests}
{
"flightId": "FL-2026-0314",
"passengerId": "USR-12345",
"class": "economy",
"departureDate": "2026-04-15T00:00:00Z",
"notes": "Window seat preferred"
}
Request Rules:
- Body = JSON only (no XML, no form-data for API)
- Dates = ISO 8601 UTC (
2026-04-15T10:30:00Z) - IDs = string (not integer) — allows UUID, formatted IDs
- Amounts = decimal, separate
currencyfield - Booleans =
true/false(not 0/1, not "yes"/"no")
3.2 Success Response
// 201 Created
// Location: /v1/travel/bookings/BK-20260314-001
{
"data": {
"id": "BK-20260314-001",
"type": "booking",
"attributes": {
"flightId": "FL-2026-0314",
"passengerId": "USR-12345",
"class": "economy",
"status": "confirmed",
"totalAmount": 450.00,
"currency": "USD",
"createdAt": "2026-03-14T08:30:00Z",
"updatedAt": "2026-03-14T08:30:00Z"
}
}
}
3.3 Error Response
// 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "departureDate",
"code": "INVALID_DATE",
"message": "Departure date must be in the future"
},
{
"field": "class",
"code": "INVALID_VALUE",
"message": "Class must be one of: economy, business, first"
}
],
"traceId": "abc123-def456"
}
}
Error Response Rules:
- Always include
traceId(for debugging in App Insights) code= machine-readable (for client logic). SCREAMING_SNAKE_CASEmessage= human-readable (for developer). Englishdetails[]= per-field validation errors- NEVER expose stack traces, SQL queries, or internal paths in error responses
- NEVER expose which field is wrong for auth errors (prevents enumeration)
3.4 Standard Error Codes
| Code | HTTP Status | Meaning |
|---|---|---|
VALIDATION_ERROR |
400 | Input validation failed |
AUTHENTICATION_REQUIRED |
401 | Missing or invalid token |
FORBIDDEN |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource doesn't exist |
CONFLICT |
409 | Business rule conflict |
BUSINESS_RULE_VIOLATION |
422 | Valid input but domain rejects |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Unexpected error |
SERVICE_UNAVAILABLE |
503 | Dependency down or overloaded |
4. Pagination, Filtering, Sorting
4.1 Pagination (Cursor-based preferred)
GET /v1/travel/bookings?limit=20&after=BK-20260314-001
Response:
{
"data": [ ... ],
"pagination": {
"limit": 20,
"hasMore": true,
"cursors": {
"before": "BK-20260301-100",
"after": "BK-20260314-020"
}
}
}
Why cursor-based over offset?
- Offset:
?page=5&size=20→ inconsistent with real-time data (inserts shift pages) - Cursor:
?after=lastId&limit=20→ stable, performant (no OFFSET scan), works with event-driven data
Fallback: Offset pagination allowed for Reporting Service (static data, simpler queries):
GET /v1/reports/bookings-summary?page=1&pageSize=50
Response includes:
"pagination": { "page": 1, "pageSize": 50, "totalItems": 1234, "totalPages": 25 }
4.2 Filtering
GET /v1/travel/bookings?status=confirmed&departureDate.gte=2026-04-01&departureDate.lte=2026-04-30
Filter patterns:
?field=value Exact match
?field.gte=value Greater than or equal
?field.lte=value Less than or equal
?field.contains=text Partial match (LIKE %text%)
?field.in=a,b,c Multiple values (OR)
4.3 Sorting
GET /v1/travel/bookings?sort=-createdAt,+status
Convention:
+field = ascending (default if no prefix)
-field = descending
Multiple: comma-separated, applied in order
5. API Versioning
5.1 Strategy: URL Path Versioning
/v1/travel/bookings ← Current
/v2/travel/bookings ← Future (when breaking changes needed)
Why URL path over header versioning?
- Explicit: developer sees version in URL
- Cacheable: CDN can cache per-version
- Simple: no custom header parsing
- Debuggable: visible in logs, traces, browser
5.2 Versioning Rules
RULE 1: v1 for all services at launch. No premature versioning.
RULE 2: Breaking change = new version. Non-breaking = same version.
Breaking: Remove field, rename field, change type, change behavior
Non-breaking: Add optional field, add new endpoint, deprecate (not remove)
RULE 3: Support N-1 versions minimum (v1 + v2 simultaneously).
Sunset v1 after 6 months of v2 availability.
RULE 4: Deprecation flow:
1. Add `Sunset: <date>` header to v1 responses
2. Add `Deprecation: true` header
3. Log warning when v1 called (track usage)
4. Remove v1 only when usage → 0
6. Cross-Cutting Concerns
6.1 Standard Headers
| Header | Direction | Required | Purpose |
|---|---|---|---|
Authorization |
Request | Yes (except public) | Bearer JWT token |
Content-Type |
Both | Yes | application/json |
X-Correlation-Id |
Both | Auto-generated | Distributed tracing |
X-Idempotency-Key |
Request | Required for POST | Prevent duplicate creates |
X-Request-Id |
Response | Always | Unique per-request ID |
Retry-After |
Response | On 429 | Seconds until retry |
Sunset |
Response | On deprecation | Date when version removed |
Cache-Control |
Response | GET responses | Caching policy |
6.2 Idempotency
POST /v1/travel/bookings
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Server behavior:
1. Check if idempotency key exists in cache (Redis or DB)
2. If exists → return cached response (same status + body)
3. If not → process request, cache response, return
4. Key TTL: 24 hours
Why: Network retries, client timeouts, double-clicks.
Without: Same booking created twice.
6.3 Rate Limiting
Per-user limits (JWT claim → user ID):
Standard user: 100 req/min
Manager: 200 req/min
Admin: 500 req/min
System (S2S): 1000 req/min
Per-endpoint overrides:
POST /bookings: 20 req/min (expensive operation)
GET /reports/export: 5 req/min (heavy computation)
GET /bookings: 200 req/min (read-heavy, OK)
Response when limited:
429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710410400
6.4 HATEOAS (Minimal)
// We use lightweight links, not full HATEOAS
{
"data": {
"id": "BK-20260314-001",
"type": "booking",
"attributes": { ... },
"links": {
"self": "/v1/travel/bookings/BK-20260314-001",
"passengers": "/v1/travel/bookings/BK-20260314-001/passengers",
"cancel": "/v1/travel/bookings/BK-20260314-001"
}
}
}
7. OpenAPI / Swagger
7.1 Standard
# Every service MUST have openapi.yaml at /swagger/v1/swagger.json
openapi: 3.1.0
info:
title: Travel Service API
version: 1.0.0
description: Manages travel bookings, passengers, and itineraries
contact:
name: D2 (Travel Service Owner)
servers:
- url: https://api.product.com/v1/travel
description: Production
- url: https://staging-api.product.com/v1/travel
description: Staging
# Auto-generated by Swashbuckle from C# attributes + XML comments
# Manually reviewed for accuracy before publishing
7.2 Documentation Rules
RULE 1: Every endpoint MUST have:
- Summary (1 line)
- Description (1 paragraph)
- Request/response examples
- Error responses documented (400, 401, 403, 404, 422)
RULE 2: Swagger UI enabled on Dev + Staging. Disabled on Production.
RULE 3: OpenAPI spec is the contract.
- Pact tests generated FROM or validated AGAINST OpenAPI spec
- Frontend team consumes OpenAPI to generate TypeScript types
RULE 4: Breaking spec change = PR review by D1 + affected consumer teams
8. ACL API Design (Special Case)
Payment ACL: Bridge between new services and legacy monolith
INTERNAL API (new services → ACL):
POST /v1/acl/payments/authorize
POST /v1/acl/payments/capture
POST /v1/acl/payments/refund
GET /v1/acl/payments/{transactionId}/status
RULES:
1. ACL translates modern API → legacy format internally
2. New services NEVER know about legacy API structure
3. ACL validates + sanitizes data in BOTH directions
4. Timeout: 5s hard limit. Circuit breaker at 3 failures / 30s
5. Fallback: Return "payment_pending" status → retry later
Response format: Same as all other services (standard error format)
→ Consumers don't know they're talking to legacy behind the scenes
9. API Review Checklist
For every new endpoint (PR review):
□ URL follows kebab-case, plural nouns convention
□ Correct HTTP method (GET = read, POST = create, etc.)
□ Correct status codes (201 for create, 204 for delete)
□ Request validation (FluentValidation + 400 response)
□ Standard error response format used
□ Authorization check present (role + resource level)
□ Pagination for list endpoints
□ Idempotency key for POST endpoints
□ Rate limiting configured
□ OpenAPI documentation complete (summary, description, examples)
□ Pact contract created/updated
□ No PII in URL paths or query parameters
□ Correlation ID propagated
□ Log entry for business operation (structured, no PII)