🌐 REST APIs

REpresentational State Transfer

What is a REST API?

REST (REpresentational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources.

🏗️ REST Principles

// 1. Client-Server Architecture
// Client and server are separate, communicate via HTTP

// 2. Stateless
// Each request contains all information needed
// Server doesn't store client state between requests

// 3. Cacheable
// Responses should define themselves as cacheable or not

// 4. Uniform Interface
// Standardized way to communicate (HTTP methods, URIs, etc.)

// 5. Layered System
// Client doesn't know if connected directly to server or through intermediary

// 6. Resource-Based
// Everything is a resource identified by URI
// Resources manipulated using representations (JSON, XML, etc.)

📍 HTTP Methods (CRUD)

// GET - Retrieve resource(s)
// Safe: doesn't modify data
// Idempotent: same result every time
fetch('/api/users')           // Get all users
fetch('/api/users/123')       // Get user with ID 123
fetch('/api/users/123/posts') // Get posts by user 123

// POST - Create new resource
// Not idempotent: creates new resource each time
fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        name: 'John',
        email: 'john@example.com'
    })
})

// PUT - Update/Replace entire resource
// Idempotent: same result if repeated
fetch('/api/users/123', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com',
        age: 30  // All fields required
    })
})

// PATCH - Partial update
// Not necessarily idempotent
fetch('/api/users/123', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        age: 31  // Only update age
    })
})

// DELETE - Remove resource
// Idempotent: same result if repeated
fetch('/api/users/123', {
    method: 'DELETE'
})

// HEAD - Like GET but only headers (no body)
fetch('/api/users/123', { method: 'HEAD' })

// OPTIONS - Get supported methods
fetch('/api/users', { method: 'OPTIONS' })

🔢 HTTP Status Codes

// 2xx Success
200  // OK - Request succeeded
201  // Created - Resource created (POST)
202  // Accepted - Request accepted, processing
204  // No Content - Success but no response body (DELETE)

// 3xx Redirection
301  // Moved Permanently
302  // Found - Temporary redirect
304  // Not Modified - Cached version still valid

// 4xx Client Errors
400  // Bad Request - Invalid syntax/data
401  // Unauthorized - Authentication required
403  // Forbidden - Authenticated but not allowed
404  // Not Found - Resource doesn't exist
405  // Method Not Allowed - HTTP method not supported
409  // Conflict - Request conflicts with current state
422  // Unprocessable Entity - Validation errors
429  // Too Many Requests - Rate limit exceeded

// 5xx Server Errors
500  // Internal Server Error - Generic server error
502  // Bad Gateway - Invalid response from upstream
503  // Service Unavailable - Server temporarily down
504  // Gateway Timeout - Upstream timeout

// Check status code
let response = await fetch('/api/users');

if (response.status === 200) {
    let data = await response.json();
}

if (response.status === 404) {
    console.log('User not found');
}

if (response.status >= 500) {
    console.log('Server error');
}

// Use response.ok for success
if (response.ok) {  // 200-299
    let data = await response.json();
} else {
    console.error(`HTTP ${response.status}`);
}

🛤️ URL Design Patterns

// Resources as nouns (not verbs)
// ✅ Good
GET    /api/users
POST   /api/users
GET    /api/users/123
PUT    /api/users/123
DELETE /api/users/123

// ❌ Bad
GET    /api/getAllUsers
POST   /api/createUser
GET    /api/getUserById/123
PUT    /api/updateUser/123
DELETE /api/deleteUser/123

// Hierarchical relationships
GET    /api/users/123/posts        // All posts by user 123
GET    /api/users/123/posts/456    // Post 456 by user 123
POST   /api/users/123/posts        // Create post for user 123
GET    /api/posts/456/comments     // Comments on post 456

// Query parameters for filtering/sorting/pagination
GET    /api/users?role=admin
GET    /api/users?page=2&limit=20
GET    /api/users?sort=name&order=asc
GET    /api/posts?author=123&status=published

// Versioning
GET    /api/v1/users
GET    /api/v2/users

// Or subdomain
GET    https://api-v1.example.com/users
GET    https://api-v2.example.com/users

// Plural nouns for collections
GET    /api/users     ✅
GET    /api/user      ❌

// Lowercase with hyphens
GET    /api/user-profiles     ✅
GET    /api/userProfiles      ❌
GET    /api/user_profiles     ❌

📨 Request/Response Format

Request Headers

fetch('/api/users', {
    headers: {
        // Content type of request body
        'Content-Type': 'application/json',
        
        // Accepted response format
        'Accept': 'application/json',
        
        // Authentication
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
        
        // API key
        'X-API-Key': 'your-api-key',
        
        // Custom headers (prefix with X-)
        'X-Request-ID': 'abc-123',
        'X-Client-Version': '1.2.3'
    }
})

// Common Content-Type values
'application/json'                      // JSON data
'application/x-www-form-urlencoded'     // Form data
'multipart/form-data'                   // File upload
'text/plain'                            // Plain text
'text/html'                             // HTML

Response Structure

// Success response
{
    "data": {
        "id": 123,
        "name": "John Doe",
        "email": "john@example.com"
    },
    "meta": {
        "timestamp": "2024-01-15T10:30:00Z"
    }
}

// Collection response with pagination
{
    "data": [
        { "id": 1, "name": "User 1" },
        { "id": 2, "name": "User 2" }
    ],
    "meta": {
        "page": 1,
        "perPage": 20,
        "total": 100,
        "pages": 5
    },
    "links": {
        "self": "/api/users?page=1",
        "first": "/api/users?page=1",
        "prev": null,
        "next": "/api/users?page=2",
        "last": "/api/users?page=5"
    }
}

// Error response
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid input data",
        "details": [
            {
                "field": "email",
                "message": "Invalid email format"
            },
            {
                "field": "age",
                "message": "Must be 18 or older"
            }
        ]
    },
    "meta": {
        "timestamp": "2024-01-15T10:30:00Z",
        "requestId": "abc-123"
    }
}

// Created response (201)
// Include Location header with new resource URL
{
    "data": {
        "id": 124,
        "name": "New User",
        "email": "new@example.com"
    },
    "meta": {
        "location": "/api/users/124"
    }
}

🔐 Authentication & Authorization

API Key

// In header
fetch('/api/users', {
    headers: {
        'X-API-Key': 'your-api-key-here'
    }
})

// In query parameter
fetch('/api/users?api_key=your-api-key-here')

// Simple but less secure
// Use HTTPS to protect key in transit

Bearer Token (JWT)

// Login to get token
let response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        email: 'user@example.com',
        password: 'password123'
    })
});

let { token } = await response.json();

// Store token
localStorage.setItem('token', token);

// Use token in subsequent requests
fetch('/api/users', {
    headers: {
        'Authorization': `Bearer ${token}`
    }
})

// API wrapper with auto-authentication
class API {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    getToken() {
        return localStorage.getItem('token');
    }
    
    async request(endpoint, options = {}) {
        let token = this.getToken();
        
        let headers = {
            'Content-Type': 'application/json',
            ...options.headers
        };
        
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
        }
        
        let response = await fetch(`${this.baseURL}${endpoint}`, {
            ...options,
            headers
        });
        
        if (response.status === 401) {
            // Token expired, redirect to login
            localStorage.removeItem('token');
            window.location.href = '/login';
            throw new Error('Unauthorized');
        }
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    get(endpoint) {
        return this.request(endpoint);
    }
    
    post(endpoint, data) {
        return this.request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }
}

let api = new API('https://api.example.com');
let users = await api.get('/users');

OAuth 2.0

// OAuth flow (simplified)
// 1. Redirect user to authorization URL
let authUrl = 'https://provider.com/oauth/authorize?' +
    'client_id=YOUR_CLIENT_ID&' +
    'redirect_uri=https://yourapp.com/callback&' +
    'response_type=code&' +
    'scope=read write';

window.location.href = authUrl;

// 2. User approves, provider redirects back with code
// https://yourapp.com/callback?code=AUTH_CODE

// 3. Exchange code for access token
let params = new URLSearchParams(window.location.search);
let code = params.get('code');

let response = await fetch('https://provider.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        client_id: 'YOUR_CLIENT_ID',
        client_secret: 'YOUR_CLIENT_SECRET',
        redirect_uri: 'https://yourapp.com/callback'
    })
});

let { access_token, refresh_token } = await response.json();

// 4. Use access token
fetch('/api/users', {
    headers: {
        'Authorization': `Bearer ${access_token}`
    }
})

🔄 Pagination Patterns

// Offset-based pagination
GET /api/users?offset=0&limit=20    // First 20
GET /api/users?offset=20&limit=20   // Next 20

async function fetchPage(page, perPage = 20) {
    let offset = (page - 1) * perPage;
    let response = await fetch(`/api/users?offset=${offset}&limit=${perPage}`);
    return response.json();
}

// Page-based pagination
GET /api/users?page=1&per_page=20
GET /api/users?page=2&per_page=20

async function* fetchAllPages(endpoint, perPage = 20) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        let response = await fetch(`${endpoint}?page=${page}&per_page=${perPage}`);
        let data = await response.json();
        
        yield data.data;
        
        hasMore = page < data.meta.pages;
        page++;
    }
}

// Usage
for await (let users of fetchAllPages('/api/users')) {
    console.log('Page:', users);
}

// Cursor-based pagination (for large datasets)
GET /api/users?cursor=abc123&limit=20

let cursor = null;
let users = [];

while (true) {
    let url = `/api/users?limit=20${cursor ? `&cursor=${cursor}` : ''}`;
    let response = await fetch(url);
    let data = await response.json();
    
    users.push(...data.data);
    
    if (!data.meta.nextCursor) break;
    cursor = data.meta.nextCursor;
}

// Link header pagination (GitHub style)
// Response includes Link header:
// Link: ; rel="next",
//       ; rel="last"

function parseLink(header) {
    let links = {};
    let parts = header.split(',');
    
    parts.forEach(part => {
        let [url, rel] = part.split(';');
        let urlMatch = url.match(/<(.+)>/);
        let relMatch = rel.match(/rel="(.+)"/);
        
        if (urlMatch && relMatch) {
            links[relMatch[1]] = urlMatch[1];
        }
    });
    
    return links;
}

let response = await fetch('/api/users');
let linkHeader = response.headers.get('Link');
let links = parseLink(linkHeader);

if (links.next) {
    let nextResponse = await fetch(links.next);
}

🔍 Filtering, Sorting, Searching

// Filtering
GET /api/users?role=admin
GET /api/users?status=active&role=admin
GET /api/posts?author=123&published=true

// Multiple values
GET /api/users?role=admin,moderator
GET /api/users?id=1,2,3,4,5

// Comparison operators
GET /api/users?age[gte]=18         // age >= 18
GET /api/users?age[lt]=65          // age < 65
GET /api/users?created[gte]=2024-01-01

// Sorting
GET /api/users?sort=name           // Ascending
GET /api/users?sort=-name          // Descending (-)
GET /api/users?sort=name,-created  // Multiple fields

// Alternative syntax
GET /api/users?sort=name&order=asc
GET /api/users?sort=name&order=desc

// Searching
GET /api/users?search=john
GET /api/users?q=javascript

// Field selection (sparse fieldsets)
GET /api/users?fields=id,name,email
GET /api/users/123?fields=name,email

// Include related resources
GET /api/posts?include=author,comments
GET /api/users/123?include=posts,profile

// Combined example
async function fetchUsers(filters = {}) {
    let params = new URLSearchParams();
    
    if (filters.role) params.append('role', filters.role);
    if (filters.status) params.append('status', filters.status);
    if (filters.search) params.append('search', filters.search);
    if (filters.sort) params.append('sort', filters.sort);
    if (filters.page) params.append('page', filters.page);
    if (filters.limit) params.append('limit', filters.limit);
    
    let url = `/api/users?${params}`;
    let response = await fetch(url);
    return response.json();
}

// Usage
let users = await fetchUsers({
    role: 'admin',
    status: 'active',
    sort: '-created',
    page: 1,
    limit: 20
});

⚠️ Error Handling

// Comprehensive error handling
async function apiRequest(url, options = {}) {
    try {
        let response = await fetch(url, options);
        
        // Check HTTP status
        if (!response.ok) {
            let error = await response.json().catch(() => ({}));
            
            switch (response.status) {
                case 400:
                    throw new ValidationError(error.message, error.details);
                case 401:
                    throw new AuthError('Unauthorized');
                case 403:
                    throw new AuthError('Forbidden');
                case 404:
                    throw new NotFoundError('Resource not found');
                case 429:
                    throw new RateLimitError('Too many requests');
                case 500:
                    throw new ServerError('Internal server error');
                default:
                    throw new APIError(`HTTP ${response.status}`, response.status);
            }
        }
        
        return response.json();
        
    } catch (error) {
        if (error instanceof TypeError) {
            // Network error
            throw new NetworkError('Network request failed');
        }
        throw error;
    }
}

// Custom error classes
class APIError extends Error {
    constructor(message, status) {
        super(message);
        this.name = 'APIError';
        this.status = status;
    }
}

class ValidationError extends APIError {
    constructor(message, details) {
        super(message, 400);
        this.name = 'ValidationError';
        this.details = details;
    }
}

class AuthError extends APIError {
    constructor(message) {
        super(message, 401);
        this.name = 'AuthError';
    }
}

class NotFoundError extends APIError {
    constructor(message) {
        super(message, 404);
        this.name = 'NotFoundError';
    }
}

class RateLimitError extends APIError {
    constructor(message) {
        super(message, 429);
        this.name = 'RateLimitError';
    }
}

class ServerError extends APIError {
    constructor(message) {
        super(message, 500);
        this.name = 'ServerError';
    }
}

class NetworkError extends Error {
    constructor(message) {
        super(message);
        this.name = 'NetworkError';
    }
}

// Usage
try {
    let user = await apiRequest('/api/users/123');
    console.log(user);
} catch (error) {
    if (error instanceof ValidationError) {
        console.error('Validation errors:', error.details);
    } else if (error instanceof AuthError) {
        redirectToLogin();
    } else if (error instanceof NotFoundError) {
        show404Page();
    } else if (error instanceof NetworkError) {
        showOfflineMessage();
    } else {
        console.error('Unexpected error:', error);
    }
}

💡 Practical Examples

Full CRUD Operations

class UserService {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }
    
    async getAll(params = {}) {
        let query = new URLSearchParams(params);
        let response = await fetch(`${this.baseURL}/users?${query}`);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    async getById(id) {
        let response = await fetch(`${this.baseURL}/users/${id}`);
        
        if (response.status === 404) {
            return null;
        }
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    async create(userData) {
        let response = await fetch(`${this.baseURL}/users`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(userData)
        });
        
        if (!response.ok) {
            let error = await response.json();
            throw new Error(error.message);
        }
        
        return response.json();
    }
    
    async update(id, userData) {
        let response = await fetch(`${this.baseURL}/users/${id}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(userData)
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    async patch(id, updates) {
        let response = await fetch(`${this.baseURL}/users/${id}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(updates)
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    async delete(id) {
        let response = await fetch(`${this.baseURL}/users/${id}`, {
            method: 'DELETE'
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.status === 204 ? null : response.json();
    }
}

// Usage
let users = new UserService('https://api.example.com');

let allUsers = await users.getAll({ page: 1, limit: 20 });
let user = await users.getById(123);
let newUser = await users.create({ name: 'John', email: 'john@example.com' });
let updated = await users.update(123, { name: 'John Doe', email: 'john@example.com' });
let patched = await users.patch(123, { email: 'newemail@example.com' });
await users.delete(123);

Rate Limiting

class RateLimiter {
    constructor(maxRequests, windowMs) {
        this.maxRequests = maxRequests;
        this.windowMs = windowMs;
        this.requests = [];
    }
    
    async throttle() {
        let now = Date.now();
        
        // Remove old requests outside window
        this.requests = this.requests.filter(
            time => now - time < this.windowMs
        );
        
        if (this.requests.length >= this.maxRequests) {
            let oldestRequest = this.requests[0];
            let waitTime = this.windowMs - (now - oldestRequest);
            
            console.log(`Rate limited. Waiting ${waitTime}ms`);
            await new Promise(resolve => setTimeout(resolve, waitTime));
            
            return this.throttle();
        }
        
        this.requests.push(now);
    }
}

// Usage
let limiter = new RateLimiter(10, 60000);  // 10 requests per minute

async function fetchWithLimit(url) {
    await limiter.throttle();
    return fetch(url);
}

Batch Requests

// Batch API pattern
async function batchFetch(ids) {
    // Single request for multiple resources
    let idsParam = ids.join(',');
    let response = await fetch(`/api/users?ids=${idsParam}`);
    return response.json();
}

// Or using POST
async function batchFetch(ids) {
    let response = await fetch('/api/users/batch', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ids })
    });
    return response.json();
}

// DataLoader pattern (automatic batching)
class DataLoader {
    constructor(batchFn, maxBatchSize = 100) {
        this.batchFn = batchFn;
        this.maxBatchSize = maxBatchSize;
        this.queue = [];
        this.cache = new Map();
    }
    
    load(key) {
        if (this.cache.has(key)) {
            return Promise.resolve(this.cache.get(key));
        }
        
        return new Promise((resolve, reject) => {
            this.queue.push({ key, resolve, reject });
            
            if (this.queue.length === 1) {
                process.nextTick(() => this.dispatch());
            }
        });
    }
    
    async dispatch() {
        let queue = this.queue.slice();
        this.queue = [];
        
        let keys = queue.map(item => item.key);
        
        try {
            let results = await this.batchFn(keys);
            
            queue.forEach((item, index) => {
                let result = results[index];
                this.cache.set(item.key, result);
                item.resolve(result);
            });
        } catch (error) {
            queue.forEach(item => item.reject(error));
        }
    }
}

// Usage
let userLoader = new DataLoader(async ids => {
    let response = await fetch(`/api/users?ids=${ids.join(',')}`);
    return response.json();
});

// These will be batched automatically
let user1Promise = userLoader.load(1);
let user2Promise = userLoader.load(2);
let user3Promise = userLoader.load(3);

let [user1, user2, user3] = await Promise.all([
    user1Promise,
    user2Promise,
    user3Promise
]);
// Single request: GET /api/users?ids=1,2,3

🎯 Key Takeaways