Understanding RESTful APIs: Conventions and Documentation

A comprehensive guide to RESTful API design and documentation

RESTful API Conventions

Think of a RESTful API like a well-organized library. Just as a library has standard ways to organize and access books, RESTful APIs have standard patterns for organizing and accessing resources.

Core Resource Endpoints


// Example: Book Resource API

// 1. Get all books
GET /books
Response: {
    "books": [
        { "id": 1, "title": "The Great Gatsby" },
        { "id": 2, "title": "1984" }
    ],
    "total": 2
}

// 2. Create a new book
POST /books
Request Body: {
    "title": "Brave New World",
    "author": "Aldous Huxley",
    "year": 1932
}
Response: {
    "id": 3,
    "title": "Brave New World",
    "author": "Aldous Huxley",
    "year": 1932,
    "created_at": "2024-03-15T10:30:00Z"
}

// 3. Get a specific book
GET /books/3
Response: {
    "id": 3,
    "title": "Brave New World",
    "author": "Aldous Huxley",
    "year": 1932
}

// 4. Update a book
PUT /books/3
Request Body: {
    "title": "Brave New World",
    "author": "Aldous Huxley",
    "year": 1932,
    "edition": "Revised"
}

// 5. Delete a book
DELETE /books/3
Response: {
    "message": "Book successfully deleted",
    "id": 3
}

Nested Resources

Nested resources represent relationships between different types of data. For example, books can have reviews:


// Get all reviews for a book
GET /books/3/reviews
Response: {
    "reviews": [
        {
            "id": 1,
            "rating": 5,
            "text": "A masterpiece!",
            "user_id": 42
        },
        {
            "id": 2,
            "rating": 4,
            "text": "Thought-provoking",
            "user_id": 57
        }
    ]
}

// Add a new review to a book
POST /books/3/reviews
Request Body: {
    "rating": 5,
    "text": "Changed my perspective",
    "user_id": 101
}

// Update a specific review
PUT /reviews/2
Request Body: {
    "rating": 3,
    "text": "Updated thoughts after second reading"
}

// Delete a specific review
DELETE /reviews/2

Special Resource Patterns

Some resources might have special patterns that don't follow standard CRUD operations:


// Current weather example - Special identifier
GET /weather/current
Response: {
    "temperature": 72,
    "conditions": "sunny",
    "timestamp": "2024-03-15T15:30:00Z"
}

// Search functionality
GET /books/search?q=brave+new&genre=fiction
Response: {
    "results": [
        {
            "id": 3,
            "title": "Brave New World",
            "relevance": 0.95
        }
    ],
    "total": 1
}

// Batch operations
POST /books/batch
Request Body: {
    "operation": "delete",
    "ids": [1, 2, 3]
}
Response: {
    "success": true,
    "deleted": 3,
    "failed": 0
}

API Documentation Best Practices

1. Endpoint Documentation


/**
 * Create Book
 * POST /api/v1/books
 * 
 * Creates a new book in the library system.
 * 
 * Request Body:
 * {
 *   "title": string (required) - Book title
 *   "author": string (required) - Author's name
 *   "isbn": string (required) - Valid ISBN-13
 *   "genre": string[] - Array of genres
 *   "published_date": string - ISO 8601 date
 * }
 * 
 * Response: 201 Created
 * {
 *   "id": number - Unique book identifier
 *   "title": string - Book title
 *   "author": string - Author's name
 *   "isbn": string - ISBN-13
 *   "genre": string[] - Array of genres
 *   "published_date": string - ISO 8601 date
 *   "created_at": string - ISO 8601 timestamp
 * }
 * 
 * Errors:
 * - 400 Bad Request: Invalid input data
 * - 409 Conflict: ISBN already exists
 * - 422 Unprocessable Entity: Invalid ISBN format
 */

// Implementation example
async function createBook(bookData) {
    const response = await fetch('/api/v1/books', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(bookData)
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message);
    }

    return response.json();
}

2. Query Parameters


/**
 * List Books
 * GET /api/v1/books
 * 
 * Returns a paginated list of books.
 * 
 * Query Parameters:
 * - page (number, optional): Page number, default: 1
 * - limit (number, optional): Items per page, default: 20
 * - sort (string, optional): Sort field, e.g., "title", "-published_date"
 * - genre (string, optional): Filter by genre
 * - search (string, optional): Search in title and author
 * 
 * Response: 200 OK
 * {
 *   "books": Book[],
 *   "total": number,
 *   "page": number,
 *   "total_pages": number
 * }
 */

// Implementation example
function getQueryString(params) {
    const searchParams = new URLSearchParams();
    
    for (const [key, value] of Object.entries(params)) {
        if (value !== undefined) {
            searchParams.append(key, value);
        }
    }
    
    return searchParams.toString();
}

async function listBooks(params = {}) {
    const queryString = getQueryString(params);
    const response = await fetch(`/api/v1/books?${queryString}`);
    
    if (!response.ok) {
        throw new Error('Failed to fetch books');
    }
    
    return response.json();
}

Error Handling Patterns


/**
 * Standard Error Response Format
 * {
 *   "error": {
 *     "code": string,
 *     "message": string,
 *     "details": object[],
 *     "request_id": string
 *   }
 * }
 */

// Error handling middleware example
function errorHandler(err, req, res, next) {
    const error = {
        code: err.code || 'INTERNAL_ERROR',
        message: err.message || 'An unexpected error occurred',
        request_id: req.id
    };

    if (err.details) {
        error.details = err.details;
    }

    const status = err.status || 500;
    res.status(status).json({ error });
}

// Validation error example
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid input data",
        "details": [
            {
                "field": "isbn",
                "message": "Invalid ISBN format"
            },
            {
                "field": "published_date",
                "message": "Must be a valid date"
            }
        ],
        "request_id": "req_abc123"
    }
}

Rate Limiting and Authentication


/**
 * Authentication Headers
 * Authorization: Bearer your-token-here
 * 
 * Rate Limit Headers
 * X-RateLimit-Limit: 100
 * X-RateLimit-Remaining: 95
 * X-RateLimit-Reset: 1615825200
 */

async function authenticatedRequest(endpoint, options = {}) {
    const token = await getAuthToken();
    
    const response = await fetch(endpoint, {
        ...options,
        headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json',
            ...options.headers
        }
    });
    
    // Check rate limits
    const rateLimit = {
        limit: response.headers.get('X-RateLimit-Limit'),
        remaining: response.headers.get('X-RateLimit-Remaining'),
        reset: response.headers.get('X-RateLimit-Reset')
    };
    
    if (rateLimit.remaining === '0') {
        const resetDate = new Date(rateLimit.reset * 1000);
        throw new Error(`Rate limit exceeded. Resets at ${resetDate}`);
    }
    
    return response;
}