Learn API development fundamentals and best practices for building scalable integrations. Covers REST, GraphQL, security, and documentation strategies.
Why API Design Matters
APIs are the backbone of modern software. They connect mobile apps to backends, enable third-party integrations, power microservices architectures, and expose business capabilities to partners and customers.
A well-designed API:
- Reduces integration time from weeks to hours
- Minimises support burden through clear, predictable behaviour
- Scales gracefully under increasing load
- Enables ecosystem growth and partner adoption
A poorly designed API:
- Creates friction for every consumer
- Generates endless support tickets
- Becomes a bottleneck as usage grows
- Accumulates technical debt that compounds over time
This guide covers the essential patterns, security practices, and architectural decisions for building APIs that scale.
REST vs GraphQL: Choosing the Right Approach
When to Use REST
REST (Representational State Transfer) remains the dominant API architecture for good reason: simplicity, familiarity, and tooling maturity.
REST is ideal for:
| Use Case | Why REST Works |
|---|---|
| CRUD operations | Natural mapping to HTTP methods |
| Public APIs | Widely understood, excellent tooling |
| Simple data structures | Straightforward resource representation |
| Caching requirements | HTTP caching works natively |
| Broad client compatibility | Every language/platform supports REST |
REST Example: User Endpoint
1// GET /api/users/1232{3 "id": 123,4 "name": "John Smith",5 "email": "john@example.com",6 "created_at": "2026-01-06T10:30:00Z",7 "links": {8 "self": "/api/users/123",9 "orders": "/api/users/123/orders",10 "profile": "/api/users/123/profile"11 }12}When to Use GraphQL
GraphQL excels when clients need flexibility in data fetching—particularly mobile apps and complex frontends.
GraphQL is ideal for:
| Use Case | Why GraphQL Works |
|---|---|
| Mobile applications | Minimise data transfer, avoid over-fetching |
| Complex, nested data | Single request for related entities |
| Rapidly evolving frontends | Clients define their data needs |
| Multiple client types | Different views need different data shapes |
| Real-time subscriptions | Built-in subscription support |
GraphQL Example: Flexible Query
1# Client requests exactly what it needs2query GetUserWithOrders {3 user(id: 123) {4 name5 email6 orders(last: 5) {7 id8 total9 status10 items {11 product {12 name13 price14 }15 }16 }17 }18}Comparison Matrix
| Factor | REST | GraphQL |
|---|---|---|
| Learning curve | Low | Medium |
| Tooling maturity | Excellent | Good |
| Caching | Native HTTP caching | Requires custom setup |
| Over-fetching | Common problem | Solved by design |
| Under-fetching | Multiple requests needed | Single request |
| Versioning | URL or header versioning | Schema evolution |
| Error handling | HTTP status codes | Custom error format |
| File uploads | Straightforward | More complex |
Core API Design Principles
1. Consistent Resource Naming
1# Good - Nouns, plural, lowercase, hyphens2GET /api/users3GET /api/users/1234GET /api/users/123/orders5GET /api/order-items67# Bad - Verbs, inconsistent, underscores8GET /api/getUser/1239GET /api/Users10GET /api/user_orders11POST /api/createOrder2. Use HTTP Methods Correctly
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource entirely | Yes | No |
| PATCH | Partial update | Yes | No |
| DELETE | Remove resource | Yes | No |
3. Meaningful HTTP Status Codes
1// Success codes2200 OK // Standard success3201 Created // Resource created (POST)4204 No Content // Success with no response body (DELETE)56// Client errors7400 Bad Request // Invalid request syntax/parameters8401 Unauthorized // Authentication required9403 Forbidden // Authenticated but not authorised10404 Not Found // Resource doesn't exist11409 Conflict // Resource conflict (duplicate, version mismatch)12422 Unprocessable Entity // Validation errors13429 Too Many Requests // Rate limit exceeded1415// Server errors16500 Internal Server Error // Unexpected server error17502 Bad Gateway // Upstream service error18503 Service Unavailable // Temporary overload/maintenance4. Structured Error Responses
1// Consistent error format2interface ApiError {3 error: {4 code: string; // Machine-readable error code5 message: string; // Human-readable description6 details?: { // Field-level errors for validation7 field: string;8 message: string;9 }[];10 request_id: string; // For debugging/support11 };12}1314// Example response15{16 "error": {17 "code": "VALIDATION_ERROR",18 "message": "Request validation failed",19 "details": [20 { "field": "email", "message": "Invalid email format" },21 { "field": "age", "message": "Must be 18 or older" }22 ],23 "request_id": "req_abc123xyz"24 }25}Pagination, Filtering, and Sorting
Pagination Patterns
Offset-based (simple, but has issues at scale):
1GET /api/users?limit=20&offset=40Cursor-based (better for large datasets):
1GET /api/users?limit=20&cursor=eyJpZCI6MTAwfQImplementation Example:
1// Response with pagination metadata2{3 "data": [...],4 "pagination": {5 "total": 1250,6 "limit": 20,7 "has_more": true,8 "next_cursor": "eyJpZCI6MTIwfQ",9 "prev_cursor": "eyJpZCI6MTAwfQ"10 }11}Filtering and Sorting
1# Filtering2GET /api/orders?status=pending&customer_id=12334# Sorting5GET /api/products?sort=price&order=asc67# Combined8GET /api/orders?status=shipped&sort=created_at&order=desc&limit=50Authentication and Authorisation
Authentication Methods
| Method | Use Case | Pros | Cons |
|---|---|---|---|
| API Keys | Server-to-server, simple integrations | Easy to implement | No user context, limited security |
| OAuth 2.0 | Third-party access, user delegation | Industry standard, secure | Complex implementation |
| JWT | Stateless auth, microservices | Scalable, self-contained | Token size, no revocation |
| Session-based | Web applications | Simple, revocable | Requires server state |
JWT Implementation
1// JWT structure2interface JwtPayload {3 sub: string; // Subject (user ID)4 iat: number; // Issued at5 exp: number; // Expiration6 iss: string; // Issuer7 aud: string; // Audience8 scope: string[]; // Permissions9}1011// Example payload12{13 "sub": "user_123",14 "iat": 1704528000,15 "exp": 1704531600,16 "iss": "api.yourcompany.com",17 "aud": "yourcompany.com",18 "scope": ["read:users", "write:orders"]19}Authorisation Patterns
Role-Based Access Control (RBAC):
1// Permission check middleware2const requirePermission = (permission: string) => {3 return (req: Request, res: Response, next: NextFunction) => {4 const userPermissions = req.user?.permissions || [];56 if (!userPermissions.includes(permission)) {7 return res.status(403).json({8 error: {9 code: "FORBIDDEN",10 message: "Insufficient permissions",11 required: permission12 }13 });14 }1516 next();17 };18};1920// Usage21app.delete(22 "/api/users/:id",23 authenticate,24 requirePermission("users:delete"),25 deleteUser26);Rate Limiting
Rate limiting protects your API from abuse and ensures fair resource allocation.
Rate Limiting Algorithms
| Algorithm | Description | Best For |
|---|---|---|
| Fixed Window | Count requests per time window | Simple implementation |
| Sliding Window | Rolling window of requests | Smoother limits |
| Token Bucket | Tokens replenish over time | Handling bursts |
| Leaky Bucket | Constant drain rate | Consistent throughput |
Implementation Example
1// Rate limit configuration2interface RateLimitConfig {3 window_ms: number; // Time window in milliseconds4 max_requests: number; // Maximum requests per window5 key_generator: (req: Request) => string; // Identifier6}78// Different limits for different endpoints9const rateLimits = {10 default: { window_ms: 60000, max_requests: 100 },11 auth: { window_ms: 900000, max_requests: 5 }, // 5 per 15 min12 search: { window_ms: 60000, max_requests: 30 },13 export: { window_ms: 3600000, max_requests: 10 }, // 10 per hour14};1516// Response headers17X-RateLimit-Limit: 10018X-RateLimit-Remaining: 8719X-RateLimit-Reset: 1704528060Real-World Examples
| Service | Limit | Notes |
|---|---|---|
| GitHub API | 5,000/hour (authenticated) | 60/hour unauthenticated |
| Stripe API | 100/second | Higher for production |
| Twitter API | 300/15min (tweets) | Varies by endpoint |
| OpenAI API | Tokens per minute | Based on model and tier |
API Security Best Practices
OWASP API Security Top 10
| Risk | Description | Mitigation |
|---|---|---|
| Broken Object Level Auth | Accessing others' resources | Verify ownership on every request |
| Broken Authentication | Weak auth mechanisms | Strong passwords, MFA, secure tokens |
| Broken Object Property Auth | Mass assignment attacks | Explicit allowlists for writable fields |
| Unrestricted Resource Consumption | DoS via resource exhaustion | Rate limiting, pagination limits |
| Broken Function Level Auth | Accessing admin functions | Role-based access control |
| Unrestricted Access to Sensitive Flows | Automated attacks on business flows | CAPTCHA, anomaly detection |
| Server-Side Request Forgery | Internal network access | URL validation, network segmentation |
| Security Misconfiguration | Default configs, verbose errors | Security hardening, minimal info |
| Improper Inventory Management | Shadow APIs, old versions | API catalogue, version sunset policies |
| Unsafe Consumption of APIs | Trusting third-party data | Validate all external input |
Security Implementation Checklist
- HTTPS only—never accept HTTP
- Input validation—validate all parameters
- Output encoding—prevent injection in responses
- Authentication on all endpoints—no exceptions
- Authorisation checks—verify permissions
- Rate limiting—prevent abuse
- Request size limits—prevent resource exhaustion
- Logging and monitoring—detect anomalies
- CORS configuration—restrict origins appropriately
- Security headers—CSP, HSTS, X-Content-Type-Options
Input Validation Example
1import { z } from "zod";23// Define schema4const CreateUserSchema = z.object({5 email: z.string().email().max(255),6 name: z.string().min(1).max(100),7 age: z.number().int().min(18).max(150).optional(),8 role: z.enum(["user", "admin"]).default("user"),9});1011// Validation middleware12const validate = (schema: z.ZodSchema) => {13 return (req: Request, res: Response, next: NextFunction) => {14 try {15 req.body = schema.parse(req.body);16 next();17 } catch (error) {18 if (error instanceof z.ZodError) {19 return res.status(422).json({20 error: {21 code: "VALIDATION_ERROR",22 message: "Invalid request data",23 details: error.errors.map(e => ({24 field: e.path.join("."),25 message: e.message26 }))27 }28 });29 }30 next(error);31 }32 };33};GraphQL-Specific Security
GraphQL introduces unique security considerations:
Query Complexity Limiting
1// Prevent expensive queries2const complexityLimit = 1000;34const complexityRules = {5 User: {6 complexity: 1,7 orders: { complexity: 10, multiplier: "first" }8 },9 Order: {10 complexity: 5,11 items: { complexity: 2, multiplier: "first" }12 }13};1415// Reject queries exceeding limit16query TooComplex {17 users(first: 100) { # 100 * 1 = 10018 orders(first: 50) { # 100 * 50 * 10 = 50,000 <- Rejected!19 items(first: 10) {20 product { name }21 }22 }23 }24}Depth Limiting
1// Prevent deeply nested queries2const maxDepth = 5;34// This query would be rejected (depth = 6)5query TooDeep {6 user {7 orders {8 items {9 product {10 category {11 parent { # Depth 6 - rejected12 name13 }14 }15 }16 }17 }18 }19}Batch Attack Prevention
1// Limit aliased queries2const maxAliases = 10;34// Attacker attempts batching5query BatchAttack {6 u1: user(id: 1) { password } # Alias 17 u2: user(id: 2) { password } # Alias 28 # ... 10,000 more aliases9}API Versioning Strategies
URL Versioning
1GET /api/v1/users2GET /api/v2/usersPros: Clear, cacheable, easy routing
Cons: URL pollution, harder deprecation
Header Versioning
1GET /api/users2Accept: application/vnd.api+json;version=2Pros: Clean URLs, flexible
Cons: Less visible, harder to test
Query Parameter Versioning
1GET /api/users?version=2Pros: Easy to implement
Cons: Not cacheable, feels hacky
Recommended: Semantic Versioning for APIs
| Version Change | When to Use |
|---|---|
| Patch (v1.0.1) | Bug fixes, no API changes |
| Minor (v1.1.0) | Backward-compatible additions |
| Major (v2.0.0) | Breaking changes |
API Documentation
Documentation Must-Haves
| Element | Purpose |
|---|---|
| Quick start guide | Get users to first API call fast |
| Authentication | How to obtain and use credentials |
| Endpoint reference | Every endpoint, parameter, response |
| Code examples | Multiple languages, copy-paste ready |
| Error reference | All error codes and meanings |
| Rate limits | Limits and how to handle them |
| Changelog | What changed between versions |
| SDKs | Official libraries if available |
OpenAPI Specification Example
1openapi: 3.0.02info:3 title: User API4 version: 1.0.05paths:6 /users/{id}:7 get:8 summary: Get user by ID9 parameters:10 - name: id11 in: path12 required: true13 schema:14 type: integer15 responses:16 '200':17 description: User found18 content:19 application/json:20 schema:21 $ref: '#/components/schemas/User'22 '404':23 description: User not found24components:25 schemas:26 User:27 type: object28 properties:29 id:30 type: integer31 email:32 type: string33 format: email34 name:35 type: stringPerformance and Scalability
Caching Strategies
1// HTTP caching headers2Cache-Control: public, max-age=3600 // Cache 1 hour3Cache-Control: private, max-age=600 // Private cache 10 min4Cache-Control: no-store // Never cache5ETag: "abc123" // Conditional requests67// Conditional request flow8// 1. Client sends: If-None-Match: "abc123"9// 2. Server checks: ETag matches?10// 3. If match: 304 Not Modified (no body)11// 4. If changed: 200 OK with new data and ETagDatabase Query Optimisation
| Technique | When to Use |
|---|---|
| Select specific columns | Always—avoid SELECT * |
| Index filtered columns | Frequently queried fields |
| Connection pooling | High-traffic APIs |
| Query result caching | Expensive, repeated queries |
| Read replicas | Read-heavy workloads |
Async Processing for Long Operations
1// Long-running operation pattern2// POST /api/reports3// Response: 202 Accepted4{5 "job_id": "job_xyz123",6 "status": "queued",7 "status_url": "/api/jobs/job_xyz123",8 "estimated_completion": "2026-01-06T12:00:00Z"9}1011// GET /api/jobs/job_xyz12312{13 "job_id": "job_xyz123",14 "status": "completed",15 "result_url": "/api/reports/rpt_abc789",16 "completed_at": "2026-01-06T11:55:00Z"17}Monitoring and Observability
Key Metrics to Track
| Metric | Target | Action Threshold |
|---|---|---|
| Response time (p50) | < 100ms | > 200ms |
| Response time (p99) | < 500ms | > 1s |
| Error rate | < 0.1% | > 1% |
| Availability | > 99.9% | < 99.5% |
| Requests per second | Monitor trend | Sudden spikes |
Structured Logging
1// Every request should log2{3 "timestamp": "2026-01-06T10:30:00Z",4 "request_id": "req_abc123",5 "method": "POST",6 "path": "/api/orders",7 "status": 201,8 "duration_ms": 45,9 "user_id": "user_123",10 "ip": "203.0.113.50",11 "user_agent": "MyApp/1.0"12}About Buun Group
At Buun Group, we design and build APIs that power business growth. Our approach:
- API-first design: APIs as products, not afterthoughts
- Security by default: Authentication, authorisation, rate limiting built in
- Documentation: OpenAPI specs, SDKs, and developer guides
- Scalability: Architectures that handle growth
Whether you're building a SaaS platform, mobile app backend, or partner integration layer, we can help you design APIs that developers love to use.
Need help designing your API?
Topics
Comments
Sign in to join the conversation
LoginNo comments yet. Be the first to share your thoughts!
Found an issue with this article?
