Section 1 – API Metadata (Rebuilt)
The metadata section provides context about the API contract before diving into endpoints. It ensures that anyone consuming the API — whether internal developers, QA, or external partners — immediately understands the scope, ownership, authentication, and environments. A well-written metadata section prevents confusion, ensures accountability, and acts as a quick reference for governance.
Metadata Table
| Field | Description | Example (REST – SaaS App) | Example (GraphQL – B2C App) |
|---|---|---|---|
| API Name | Human-readable name of the API | User Onboarding API | Booking Service Schema |
| Version | Semantic version number (major.minor.patch) | v1.2.0 | 2025-08 schema |
| Owner | Squad/team responsible for API maintenance | Backend Team – SaaS Squad A | Mobile API Team |
| Point of Contact | Escalation contact (Slack channel, email) | #api-squad-a, api-support@company.com | #mobile-api, mobile-support@company.com |
| Base URL – Production | Endpoint for production environment | https://api.saasapp.com/v1 | https://api.ecoclean.com/graphql |
| Base URL – Staging | Endpoint for testing environment | https://staging.api.saasapp.com/v1 | https://staging.api.ecoclean.com/graphql |
| Base URL – Development | Endpoint for dev sandbox | http://localhost:8080/api/v1 | http://localhost:4000/graphql |
| Auth Mechanism | Authentication standard used | OAuth2 (Bearer Token) | Firebase Auth (JWT) |
| Scopes / Roles | Role-based access or OAuth scopes required | users.read, users.write | booking:read, booking:write |
| Rate Limits | Allowed requests per client | 100 requests/min | 60 queries/min |
| Timeout Policy | API request timeout (ms) | 30,000 ms | 20,000 ms |
| Deprecation Policy | Rules for version retirement | 90-day notice, /v1 sunset in 2026 | Schema changes flagged in changelog |
| Change Tracking | Where changes are logged | ADR Doc 12 | ADR Doc 12 |
| Monitoring & Logging | Tools and dashboards used | Datadog, Kibana | Grafana, ELK Stack |
| Error Logging Contact | Who monitors errors | DevOps Squad A | Mobile DevOps Squad |
Section 2 – Endpoints / Operations (Rebuilt)
Purpose of This Section
The Endpoints/Operations section defines the actual API contract. Unlike generic notes, this must include URLs, methods, authentication rules, required parameters, request/response bodies, and error codes. For REST APIs, endpoints are URI + HTTP method–driven; for GraphQL APIs, operations are schema-based queries and mutations. This section ensures frontend, backend, QA, and third-party consumers all speak the same language before development begins.
Table A – REST Endpoints (20 rows)
| Endpoint | Method | Description | Auth Required | Request Params | Request Body Example | Success Response (200) | Error Codes |
|---|---|---|---|---|---|---|---|
/users | POST | Create new user | Yes (Bearer) | None | { "name": "John", "email": "john@x.com", "password": "123456" } | { "id": 101, "name": "John", "email": "john@x.com" } | 400 Invalid Email, 409 Conflict |
/users/{id} | GET | Get user profile | Yes | Path: id | None | { "id": 101, "name": "John", "status": "active" } | 404 Not Found |
/users/{id} | PUT | Update user profile | Yes | Path: id | { "name": "John D", "status": "inactive" } | { "id": 101, "updatedAt": "2025-09-18T10:00:00Z" } | 400 Invalid Input |
/users/{id} | DELETE | Delete user | Admin Only | Path: id | None | { "message": "User deleted" } | 403 Forbidden, 404 Not Found |
/auth/login | POST | Authenticate user | No | None | { "email": "john@x.com", "password": "123456" } | { "token": "jwt-token", "expiresIn": 3600 } | 401 Unauthorized |
/auth/refresh | POST | Refresh access token | Yes (Refresh Token) | None | { "refreshToken": "abcd1234" } | { "token": "new-jwt-token" } | 401 Invalid Token |
/projects | GET | List projects | Yes | Query: page, limit | None | { "data": [ { "id": 1, "name": "Alpha" } ], "meta": { "page": 1 } } | 401 Unauthorized |
/projects | POST | Create project | Yes | None | { "name": "Project Alpha" } | { "id": 201, "name": "Project Alpha" } | 400 Bad Request |
/projects/{id} | GET | Get project detail | Yes | Path: id | None | { "id": 201, "name": "Alpha", "status": "active" } | 404 Not Found |
/projects/{id} | PUT | Update project | Yes | Path: id | { "status": "archived" } | { "id": 201, "status": "archived" } | 400 Invalid Input |
/tasks | GET | List tasks | Yes | Query: projectId, status | None | { "data": [ { "id": 301, "title": "Design UI" } ] } | 401 Unauthorized |
/tasks | POST | Create task | Yes | None | { "title": "Design UI", "projectId": 201 } | { "id": 301, "title": "Design UI" } | 400 Bad Request |
/tasks/{id} | GET | Get task detail | Yes | Path: id | None | { "id": 301, "title": "Design UI", "status": "open" } | 404 Not Found |
/tasks/{id} | PUT | Update task | Yes | Path: id | { "status": "done" } | { "id": 301, "status": "done" } | 400 Invalid Input |
/tasks/{id} | DELETE | Delete task | Yes | Path: id | None | { "message": "Task deleted" } | 404 Not Found |
/reports | GET | Generate report | Yes | Query: from, to | None | { "reportId": "rpt123", "status": "ready" } | 400 Invalid Dates |
/reports/{id} | GET | Download report | Yes | Path: id | None | File stream (PDF/CSV) | 404 Report Not Found |
/notifications | GET | Fetch notifications | Yes | Query: limit | None | { "data": [ { "id": 401, "text": "Task due soon" } ] } | 401 Unauthorized |
/notifications/{id} | PUT | Mark notification read | Yes | Path: id | None | { "id": 401, "status": "read" } | 404 Not Found |
/settings/profile | PUT | Update user settings | Yes | None | { "timezone": "UTC", "language": "en" } | { "id": 101, "updatedAt": "..." } | 400 Invalid Input |
Table B – GraphQL Operations (10 rows)
| Operation Type | Operation Name | Description | Example Query/Mutation | Success Response | Error Cases |
|---|---|---|---|---|---|
| Query | getUser | Fetch a user by ID | query { getUser(id: "101") { id name email } } | { "data": { "getUser": { "id": "101", "name": "John" } } } | User not found |
| Mutation | createUser | Create a new user | mutation { createUser(name:"John", email:"john@x.com") { id name } } | { "data": { "createUser": { "id": "101", "name": "John" } } } | Duplicate email |
| Mutation | updateUser | Update user details | mutation { updateUser(id:"101", status:"inactive") { id status } } | { "data": { "updateUser": { "id": "101", "status": "inactive" } } } | Invalid input |
| Query | listProjects | List all projects | query { listProjects(limit:10) { id name } } | { "data": { "listProjects": [ { "id": "201", "name": "Alpha" } ] } } | Unauthorized |
| Mutation | createProject | Add a new project | mutation { createProject(name:"Alpha") { id name } } | { "data": { "createProject": { "id": "201", "name": "Alpha" } } } | Bad request |
| Query | getProject | Get project detail | query { getProject(id:"201") { id name status } } | { "data": { "getProject": { "id": "201", "name": "Alpha" } } } | Not found |
| Mutation | updateProject | Update project detail | mutation { updateProject(id:"201", status:"archived") { id status } } | { "data": { "updateProject": { "id": "201", "status": "archived" } } } | Invalid status |
| Query | listTasks | Fetch tasks for a project | query { listTasks(projectId:"201") { id title status } } | { "data": { "listTasks": [ { "id":"301", "title":"UI" } ] } } | Unauthorized |
| Mutation | createTask | Add new task | mutation { createTask(title:"UI", projectId:"201") { id title } } | { "data": { "createTask": { "id":"301", "title":"UI" } } } | Missing projectId |
| Mutation | updateTask | Update task status | mutation { updateTask(id:"301", status:"done") { id status } } | { "data": { "updateTask": { "id":"301", "status":"done" } } } | Invalid taskId |
Section 3 – Request/Response Standards (Rebuilt)
Purpose of This Section
Request/Response standards exist to enforce uniformity across APIs, so that frontend developers, QA engineers, and third-party integrators don’t have to guess what structure to expect from one endpoint to another. Without standards, APIs become inconsistent: some return raw objects, others wrap in data, some paginate differently, and error responses vary wildly. This creates friction, wasted time, and bugs. A proper specification establishes one way of doing things, covering pagination, filtering, sorting, success envelopes, and error handling. It ensures APIs are predictable, testable, and scalable across the product lifecycle.
Table A – REST Standards
| Aspect | Contract | Example |
|---|---|---|
| HTTP Methods | Use correct verbs: GET (read), POST (create), PUT (update), DELETE (remove). PATCH for partial updates. | PUT /users/{id} updates full profile; PATCH /users/{id} updates status only. |
| Content Type | All requests and responses must use JSON with header Content-Type: application/json. | POST /users → {"name":"John"} |
| Headers | Standard headers: Authorization: Bearer <token>, Accept: application/json. | curl -H "Authorization: Bearer token" -H "Accept: application/json" |
| Pagination | Cursor-based preferred; fallback to page/limit. Always return pagination metadata. | GET /users?page=2&limit=20 → { "data":[...], "meta":{"page":2,"limit":20,"total":55} } |
| Filtering | Filters must be explicit query params; multiple filters allowed. | /users?status=active&role=admin |
| Sorting | Use query param sort with +/- prefix. Default ascending. | /projects?sort=-createdAt |
| Search | Use q param for free-text search. | /tasks?q=design |
| Success Envelope | Always wrap response in { "data":..., "meta":... }. meta optional. | { "data":{"id":101,"name":"John"} } |
| Error Format | Unified structure: { "error": { "code": <int>, "message": <string>, "details": <object> } } | { "error":{ "code":400,"message":"Invalid Email","details":{"field":"email"} } } |
| Timestamps | Always ISO 8601 UTC. | "createdAt":"2025-09-18T10:00:00Z" |
Table B – GraphQL Standards
| Aspect | Contract | Example |
|---|---|---|
| Schema | All operations must be strongly typed with nullable vs. non-nullable explicitly defined. | type User { id: ID! name: String! email: String } |
| Query Naming | Use descriptive names; avoid generic like getData. | getUser, listTasks |
| Mutation Naming | Use imperative verbs: createX, updateX, deleteX. | mutation { createTask(...) } |
| Pagination | Relay-style cursor-based (edges, pageInfo) recommended. | tasks(first:10, after:"cursor123"){ edges{node{id title}} pageInfo{hasNextPage}} |
| Filtering | Use input objects for multiple filters. | users(filter:{status:"active", role:"admin"}) { id name } |
| Sorting | Sorting fields must be explicit in schema. | users(sort:{field:"createdAt", order:DESC}) { id name } |
| Success Envelope | Always return under "data" key. | { "data": { "getUser": { "id":"101","name":"John"} } } |
| Error Format | All errors under "errors" array, with message, path, extensions. | { "errors":[ { "message":"Invalid ID", "path":["getUser"], "extensions":{"code":"400"}} ] } |
| Auth Context | Auth tokens must be passed via headers; resolvers must enforce role-level access. | Authorization: Bearer token |
| Timestamps | ISO 8601 UTC for all datetime fields. | "updatedAt":"2025-09-18T10:05:00Z" |
Section 4 – Error Codes (Rebuilt)
Purpose of This Section
Error codes define how the API communicates problems to consumers. A weak error strategy leads to chaos: frontend developers can’t distinguish between a user mistake and a server crash, QA testers can’t automate edge cases, and third-party integrators waste time guessing what went wrong. A good error catalogue ensures that every error is predictable, consistent, and actionable. This section establishes both HTTP-level status codes and application-level error codes/messages that align with the request/response standards (see Section 3).
Table A – REST Error Codes (15 Rows)
| HTTP Code | Meaning | When Used | Response Example |
|---|---|---|---|
| 200 OK | Success | Standard for successful GET, PUT, DELETE | { "data": { "id":101,"status":"active" } } |
| 201 Created | Resource created | After successful POST | { "data": { "id":201,"name":"Project Alpha" } } |
| 202 Accepted | Request accepted for async processing | Report generation, background jobs | { "data": { "reportId":"rpt123","status":"processing" } } |
| 204 No Content | Success with no body | DELETE success | Empty body |
| 400 Bad Request | Invalid input, missing params | Invalid JSON, missing email | { "error": { "code":400,"message":"Missing email" } } |
| 401 Unauthorized | Missing/invalid auth token | No Authorization header | { "error": { "code":401,"message":"Token missing/invalid" } } |
| 403 Forbidden | Valid token, insufficient rights | Non-admin trying to delete user | { "error": { "code":403,"message":"Insufficient role" } } |
| 404 Not Found | Resource doesn’t exist | Invalid id | { "error": { "code":404,"message":"User not found" } } |
| 405 Method Not Allowed | Wrong HTTP verb | POST /reports/{id} | { "error": { "code":405,"message":"Method not allowed" } } |
| 409 Conflict | Duplicate or conflicting state | Email already registered | { "error": { "code":409,"message":"Email exists" } } |
| 410 Gone | Deprecated resource | Old API version removed | { "error": { "code":410,"message":"This endpoint is deprecated" } } |
| 422 Unprocessable Entity | Semantic validation failure | Password too weak | { "error": { "code":422,"message":"Password must be 8+ chars" } } |
| 429 Too Many Requests | Rate limit exceeded | 101st request in a minute | { "error": { "code":429,"message":"Rate limit exceeded, retry in 60s" } } |
| 500 Internal Server Error | Unexpected server error | Null pointer exception | { "error": { "code":500,"message":"Unexpected server error" } } |
| 503 Service Unavailable | Temporary downtime | DB offline | { "error": { "code":503,"message":"Service temporarily unavailable" } } |
Table B – GraphQL Error Handling (10 Rows)
Unlike REST, GraphQL always returns 200 OK at HTTP level — errors are inside an errors array. We standardize error codes with extensions.code.
| Error Code | Meaning | When Used | Example Response |
|---|---|---|---|
| GRAPHQL_VALIDATION_FAILED | Invalid query syntax | Misspelled field | { "errors":[{ "message":"Cannot query field foo","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}] } |
| BAD_USER_INPUT | Invalid arguments | email missing in mutation | { "errors":[{ "message":"Email required","extensions":{"code":"BAD_USER_INPUT"}}] } |
| UNAUTHENTICATED | No/invalid token | Missing auth header | { "errors":[{ "message":"Not authenticated","extensions":{"code":"UNAUTHENTICATED"}}] } |
| FORBIDDEN | Role doesn’t allow operation | User without admin deleting project | { "errors":[{ "message":"Forbidden","extensions":{"code":"FORBIDDEN"}}] } |
| NOT_FOUND | Entity not found | Non-existent project ID | { "errors":[{ "message":"Project not found","extensions":{"code":"NOT_FOUND"}}] } |
| CONFLICT | Duplicate or inconsistent state | Duplicate email | { "errors":[{ "message":"Email exists","extensions":{"code":"CONFLICT"}}] } |
| RATE_LIMITED | Too many requests | Over API quota | { "errors":[{ "message":"Rate limit exceeded","extensions":{"code":"RATE_LIMITED"}}] } |
| INTERNAL_SERVER_ERROR | Generic backend error | Exception in resolver | { "errors":[{ "message":"Unexpected error","extensions":{"code":"INTERNAL_SERVER_ERROR"}}] } |
| SERVICE_UNAVAILABLE | Downstream service unavailable | DB timeout | { "errors":[{ "message":"Database down","extensions":{"code":"SERVICE_UNAVAILABLE"}}] } |
| DEPRECATED_FIELD | Querying removed field | Legacy schema | { "errors":[{ "message":"Field x deprecated","extensions":{"code":"DEPRECATED_FIELD"}}] } |
Section 5 – Security & Auth (Rebuilt)
Purpose of This Section
Security is not an afterthought in API design — it is the contractual backbone that ensures only authorized consumers can access resources safely. Weak documentation here leads to massive risks: developers may skip proper checks, third-party integrators may leak tokens, and testers may miss critical abuse scenarios. A strong API specification must define authentication flows, authorization rules, data protection practices, and rate-limiting policies clearly enough that no consumer can claim ignorance. This section establishes both mechanical security requirements (OAuth2, JWT, HTTPS) and behavioral policies (roles, scopes, logging, rotation, rate limits).
Table A – REST Security & Auth Standards
| Area | Contract | Example |
|---|---|---|
| Transport Security | All endpoints must use HTTPS with TLS 1.2+; no plaintext HTTP allowed. | https://api.saasapp.com/v1/users |
| Auth Mechanism | Default auth via OAuth2 (Authorization Code / Client Credentials) or JWT (Bearer token). | Authorization: Bearer <jwt-token> |
| Token Format | JWT must include sub, iat, exp, and roles claims. | { "sub":"101", "exp":1695049200, "roles":["admin"] } |
| Token Expiry | Access tokens expire in 1h; refresh tokens in 30d. | Frontend refreshes automatically before expiry. |
| Refresh Workflow | Token refresh via /auth/refresh with refresh token. | POST /auth/refresh { "refreshToken":"xyz" } |
| Scopes & Roles | Endpoints require explicit scopes (e.g., users:read, projects:write). | /users/{id} requires users:read. |
| Rate Limiting | Default: 100 requests/min per client. Higher tiers configurable. | Exceeding limit → 429 error. |
| Replay Protection | Nonces or jti (JWT ID) required for sensitive actions. | Prevents replay of /payments. |
| Logging Rules | Sensitive fields (passwords, tokens, PII) never logged in plaintext. | Log only userId, endpoint, timestamp. |
| Audit Trail | All admin-level actions logged with userId, role, timestamp. | Role change → audit entry in security_audit table. |
Table B – GraphQL Security & Auth Standards
| Area | Contract | Example |
|---|---|---|
| Transport Security | All queries and mutations over HTTPS/TLS only. | POST https://api.ecoclean.com/graphql |
| Auth Header | Every request must include Authorization: Bearer <token>. | curl -H "Authorization: Bearer jwt" ... |
| Field-Level Auth | Sensitive fields protected via resolver rules. | User.email accessible only to self or admin. |
| Role Enforcement | Mutations validated by roles/scopes. | createProject requires projects:write. |
| Rate Limiting | Limit queries per client: 60/min. Complex queries count extra. | listUsers(first:1000) = 10 ops cost. |
| Query Depth Limit | Max query depth = 5 to prevent DoS via nested queries. | users → projects → tasks → comments → likes. |
| Query Complexity Limit | Each field weighted; queries > 100 points rejected. | Weighted cost analysis. |
| Error Sanitization | Never expose stack traces; return generic error + code. | { "errors":[{ "message":"Internal Error"}] } |
| Token Expiry & Refresh | Same as REST (1h expiry, 30d refresh). | Refresh via /auth/refresh. |
| Subscription Security | WebSocket subscriptions must revalidate token on connection + periodically. | connection_init must include JWT. |
General Security Best Practices (Applies to REST + GraphQL)
- Principle of Least Privilege – Default roles (e.g.,
user) should have only minimum access; admin privileges require explicit assignment. - CORS Policy – Allowed origins explicitly listed; no wildcard
*. - Input Validation – All incoming data validated at API boundary to prevent injection attacks.
- Data Masking – Sensitive outputs (tokens, partial card numbers, SSNs) masked before returning in API responses.
- Secrets Management – Keys and tokens stored in secure vaults (HashiCorp Vault, AWS Secrets Manager).
- Versioning Security – Old versions decommissioned with minimum 90-day notice. Security patches applied retroactively until sunset.
- Monitoring & Alerts – Auth failures, rate limit violations, and suspicious traffic patterns automatically flagged to security team.
- Penetration Testing – APIs undergo regular security tests before major releases.
Closing Note
This API Specification is designed as a living contract between teams — not a static document. It provides structure, consistency, and governance across REST and GraphQL APIs, but its true value lies in being actively maintained and adapted. Every product evolves: new endpoints are added, schemas change, authentication strategies harden, and integrations scale. As such, this document should always reflect the current reality of the API, not just the intent at launch.
Depending on your product type, architectural choices, or security requirements, sections of this template may need to be extended or refined. For example, a financial SaaS may require more rigorous audit trails, while a consumer mobile app may prioritize lightweight payloads and real-time subscriptions. What must remain non-negotiable is the discipline of consistency — ensuring that every API consumer can rely on predictable standards for requests, responses, errors, and security.
In practice, this means teams should treat the specification as part of their software lifecycle governance: version it alongside code, review it during design discussions, and update it whenever breaking or non-breaking changes are introduced. Used in this way, the API Specification stops being “just documentation” and becomes an operational safeguard — reducing bugs, avoiding miscommunication, and ensuring integrations succeed.