API design is the contract between your frontend, mobile apps, third-party integrations, and your backend. A well-designed API accelerates development; a poor one creates years of technical debt. This guide covers the API design principles, patterns, and governance that scale from startup to enterprise.
REST vs GraphQL vs gRPC β The 2026 Verdict
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API STYLE DECISION MATRIX β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β USE REST WHEN: β
β β
Public/external APIs (widest adoption) β
β β
Simple CRUD resources β
β β
Team knows HTTP/REST well β
β β
Caching via HTTP semantics important β
β β
Firewall/proxy compatibility needed β
β β
Tooling ecosystem matters (OpenAPI, Postman, etc.) β
β β
β USE GRAPHQL WHEN: β
β β
Complex nested data requirements β
β β
Multiple clients with different data needs β
β β
Real-time subscriptions needed β
β β
Frontend controls data shape (over/under-fetching) β
β β
Strong TypeScript integration valued β
β β
β USE gRPC WHEN: β
β β
Internal microservices (performance critical) β
β β
Polyglot microservices (Go, Java, Python, etc.) β
β β
Streaming/bidirectional communication β
β β
Strict contract-first development β
β β
Low latency, high throughput required β
β β
β USE tRPC WHEN: β
β β
TypeScript monorepo (frontend + backend) β
β β
End-to-end type safety without codegen β
β β
Rapid iteration, minimal boilerplate β
β β
Team is TypeScript-native β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ**Our default for web apps: REST + OpenAPI + TypeScript** β broadest compatibility, excellent tooling, team familiarity.
REST API Design Principles
Resource-Oriented URLs
# β
GOOD β Nouns, plural, hierarchical
GET /api/v1/users # List users
POST /api/v1/users # Create user
GET /api/v1/users/{id} # Get user
PATCH /api/v1/users/{id} # Partial update
DELETE /api/v1/users/{id} # Delete userGET /api/v1/users/{id}/posts # User's posts POST /api/v1/users/{id}/posts # Create post for user GET /api/v1/posts/{id} # Get post (also accessible directly)
# β Sub-resources for relationships GET /api/v1/posts/{id}/comments POST /api/v1/posts/{id}/comments
# β Custom actions as sub-resources (not verbs) POST /api/v1/posts/{id}/publish POST /api/v1/users/{id}/activate POST /api/v1/orders/{id}/cancel
# β BAD β Verbs in URL, RPC-style GET /api/v1/getUsers POST /api/v1/createUser POST /api/v1/deleteUser GET /api/v1/getUserPosts?userId=123
HTTP Methods Semantics
| Method | Use Case | Idempotent | Safe | Request Body | Response | |--------|----------|------------|------|--------------|----------| | `GET` | Retrieve resource | β | β | β | 200 + resource | | `POST` | Create resource | β | β | β | 201 + created resource | | `PUT` | Full replace | β | β | β | 200 + updated resource | | `PATCH` | Partial update | β* | β | β | 200 + updated resource | | `DELETE` | Delete resource | β | β | β | 204 No Content | | `HEAD` | Metadata only | β | β | β | Headers only | | `OPTIONS` | Capabilities | β | β | β | Allow header |
*PATCH can be idempotent if implemented correctly (e.g., JSON Merge Patch).
Status Codes β Use Precisely
# Success
200 OK # GET, PUT, PATCH success
201 Created # POST success (include Location header)
204 No Content # DELETE, PATCH/PUT with no response body# Client Errors (4xx) 400 Bad Request # Invalid JSON, validation failed 401 Unauthorized # Missing/invalid auth 403 Forbidden # Auth valid but insufficient permissions 404 Not Found # Resource doesn't exist 409 Conflict # Resource conflict (duplicate email, etc.) 422 Unprocessable Entity # Valid JSON but semantic errors 429 Too Many Requests # Rate limited
# Server Errors (5xx) 500 Internal Server Error # Unexpected error 503 Service Unavailable # Maintenance, overload
Error Response Format (RFC 7807)
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Failed",
"status": 422,
"detail": "The request body failed validation",
"instance": "/api/v1/users",
"errors": [
{
"field": "email",
"code": "invalid_format",
"message": "Invalid email format"
},
{
"field": "password",
"code": "too_short",
"message": "Password must be at least 8 characters"
}
],
"trace_id": "req_abc123"
}Request/Response Standards
# List Response (paginated)
{
"data": [...],
"meta": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8
},
"links": {
"first": "/api/v1/users?page=1",
"last": "/api/v1/users?page=8",
"prev": null,
"next": "/api/v1/users?page=2"
}
}# Single Resource { "data": { "id": "usr_abc123", "type": "user", "attributes": { "name": "John Doe", "email": "john@example.com", "created_at": "2024-01-15T10:30:00Z" }, "relationships": { "posts": { "data": [ {"type": "post", "id": "post_abc"}, {"type": "post", "id": "post_def"} ], "links": { "self": "/api/v1/users/usr_abc123/relationships/posts", "related": "/api/v1/users/usr_abc123/posts" } } } }, "included": [...] }
Versioning Strategy
URL Versioning (Recommended for Public APIs)
GET /api/v1/users
GET /api/v2/users # Breaking changes in v2Header Versioning (Internal/Microservices)
Accept: application/vnd.example.v2+jsonVersioning Rules
- **Major versions in URL** (`/v1/`, `/v2/`) β Breaking changes only
- **Never break existing versions** β Add new fields, never remove/rename
- **Deprecation policy** β 12 months notice, `Deprecation` header, `Sunset` header
- **Document migration path** β Clear upgrade guide for each version
# Deprecation headers
Deprecation: true
Sunset: Sat, 01 Jan 2025 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"Filtering, Sorting, Pagination
Filtering
# Simple equality
GET /api/v1/users?status=active&role=admin# Operators GET /api/v1/users?age[gte]=18&age[lte]=65 GET /api/v1/posts?title[cont]=typescript GET /api/v1/users?status[in]=active,pending
# Complex (RQL-style) GET /api/v1/users?filter=(age>18;role=admin;name~john)
Sorting
# Single field
GET /api/v1/users?sort=created_at
GET /api/v1/users?sort=-created_at # Descending# Multiple fields GET /api/v1/users?sort=-created_at,name
Pagination β Cursor-Based (Preferred)
# First page
GET /api/v1/users?limit=20# Next page (using cursor) GET /api/v1/users?limit=20&cursor=eyJpZCI6InVzcl9hYmMifQ==
# Response { "data": [...], "meta": { "has_more": true, "next_cursor": "eyJpZCI6InVzcl9kZWYifQ==" } }
**Why cursor over offset?**
- β Consistent performance (no OFFSET scan)
- β No skipped/duplicated items on concurrent inserts
- β Works with real-time data
Authentication & Authorization
API Keys (Server-to-Server)
Authorization: Bearer sk_live_abc123...# Or custom header
X-API-Key: sk_live_abc123...JWT Bearer Tokens (User-Facing)
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...OAuth 2.0 / OIDC (Third-Party Access)
# Authorization Code Flow (web apps)
GET /oauth/authorize?client_id=...&redirect_uri=...&scope=read:users&response_type=code# Client Credentials (server-to-server) POST /oauth/token grant_type=client_credentials&client_id=...&client_secret=...&scope=read:users
Scopes/Permissions
{
"scopes": [
"read:users",
"write:users",
"read:posts",
"write:posts",
"admin:users"
]
}Rate Limiting
Standard Headers (IETF Draft)
# Response headers
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 1699900000
Retry-After: 60Rate Limit Tiers
| Tier | Requests/Minute | Burst | Use Case | |------|-----------------|-------|----------| | Free | 60 | 10 | Public API exploration | | Starter | 300 | 50 | Small apps | | Pro | 1,000 | 200 | Production apps | | Enterprise | 10,000+ | Custom | High-volume |
Webhooks
Webhook Design
# Registration
POST /api/v1/webhooks
{
"url": "https://app.example.com/webhooks",
"events": ["user.created", "order.completed", "payment.failed"],
"secret": "whsec_abc123..."
}# Delivery POST https://app.example.com/webhooks Content-Type: application/json X-Webhook-Signature: sha256=abc123... X-Webhook-Timestamp: 1699900000 X-Webhook-Id: wh_abc123
{ "id": "evt_abc123", "type": "user.created", "created_at": "2024-01-15T10:30:00Z", "data": { "object": { "id": "usr_abc", "email": "..." } } }
Webhook Best Practices
- β **HMAC signature verification** (`X-Webhook-Signature`)
- β **Timestamp validation** (reject >5 min old)
- β **Idempotency keys** (`X-Webhook-Id` for deduplication)
- β **Retry with exponential backoff** (1s, 2s, 4s, 8s, 16s, 32s, max 5 retries)
- β **Dead letter queue** for failed deliveries
- β **Test endpoint** in developer dashboard
OpenAPI/Swagger Documentation
# openapi.yaml
openapi: 3.1.0
info:
title: Example API
version: 1.0.0
description: |
Example API for managing users and posts.
contact:
name: API Support
email: api@example.com
license:
name: MITservers:
description: Production
description: Staging
- url: https://api.example.com/v1
- url: https://api-staging.example.com/v1
paths: /users: get: summary: List users parameters:
in: query schema: { type: string, enum: [active, inactive, pending] } responses: '200': description: List of users content: application/json: schema: $ref: '#/components/schemas/UserListResponse' '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/RateLimited' }
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- name: status
components: parameters: page: name: page in: query schema: { type: integer, minimum: 1, default: 1 } limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
SDK Generation
# Generate TypeScript SDK from OpenAPI
npx @openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o packages/api-client# Generated usage import { UsersApi, Configuration } from '@example/api-client';
const api = new UsersApi(new Configuration({ basePath: 'https://api.example.com/v1', accessToken: 'sk_live_...' });
const users = await api.listUsers({ page: 1, limit: 20 }); // users.data is typed: User[]
Testing Strategy
// Contract testing with Pact
// Consumer (frontend) defines expectations
// Provider (backend) verifies against contract// pact/consumer/UserApiConsumer.test.ts import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike } = MatchersV3;
const provider = new PactV3({ consumer: 'WebApp', provider: 'UserApi', });
describe('User API', () => { it('returns users list', () => { return provider .given('users exist') .uponReceiving('a request for users') .withRequest({ method: 'GET', path: '/api/v1/users', query: { page: '1', limit: '20' }, headers: { Authorization: 'Bearer token' }, }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: { data: eachLike({ id: like('usr_abc123'), name: like('John Doe'), email: like('john@example.com'), }), meta: { page: 1, per_page: 20, total: 100 }, }, }) .executeTest(async (mockServer) => { const response = await fetch(`${mockServer.url}/api/v1/users`); expect(response.ok).toBe(true); }); }); });
API Governance Checklist
Design Review Checklist
- [ ] Resources named with plural nouns
- [ ] HTTP methods used correctly
- [ ] Status codes accurate
- [ ] Error format consistent (RFC 7807)
- [ ] Versioning strategy documented
- [ ] Filtering/sorting/pagination consistent
- [ ] Auth strategy appropriate
- [ ] Rate limits documented
- [ ] OpenAPI spec complete
- [ ] Breaking change policy documented
- [ ] Deprecation policy documented
- [ ] SDKs generated and published
- [ ] Contract tests in CI
API Lifecycle
Design β Review β Implement β Contract Test β Deploy (Staging)
β Integration Test β Deploy (Production) β Monitor β DeprecateThe UI Designer API Standard
Every API we build:
- β **REST + JSON** with OpenAPI 3.1 spec
- β **Cursor-based pagination** (not offset)
- β **RFC 7807 error format**
- β **Cursor-based pagination** for lists
- β **Idempotency keys** for mutations
- β **Idempotency keys** for webhooks
- β **Rate limiting** with standard headers
- β **OpenAPI 3.1** spec + generated SDKs
- β **Contract testing** (Pact) in CI
- β **Versioning** in URL (`/v1/`, `/v2/`)
- β **Deprecation headers** + 12-month notice
Ready to Design Better APIs?
**Book a free API architecture review** β we'll audit your current API, identify design issues, and give you a prioritized improvement roadmap.
ui-designer.in/api-design

