Documents/reference/API Design Standards

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 currency field
  • 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_CASE
  • message = human-readable (for developer). English
  • details[] = 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)