Skip to content

Latest commit

 

History

History
732 lines (554 loc) · 31.1 KB

File metadata and controls

732 lines (554 loc) · 31.1 KB

Comprehensive Guide to REST APIs: A Beginner to Intermediate Study Manual

Welcome to the definitive guide on REST APIs. This manual is designed for developers, engineers, and technical enthusiasts who want to gain a deep, practical understanding of how REST APIs work, how to design them, and how to consume them.

Whether you are building your first web server or looking to solidify your understanding of backend architecture, this guide covers everything from foundational concepts to practical implementation details, complete with examples and best practices.


Table of Contents

  1. Introduction to APIs
  2. What is REST?
  3. HTTP Basics
  4. RESTful Design
  5. Request and Response Structure
  6. CRUD Operations
  7. Authentication and Authorization
  8. Error Handling
  9. Pagination, Filtering, Sorting
  10. Rate Limiting
  11. Security Best Practices
  12. Testing APIs
  13. Documentation
  14. Best Practices
  15. Common Mistakes
  16. Real-world Examples

1. Introduction to APIs

What is an API?

API stands for Application Programming Interface. At its core, an API is a set of rules, protocols, and tools that allows different software applications to communicate with each other.

The Restaurant Analogy: Imagine you are sitting at a table in a restaurant. You (the client) want to order food from the kitchen (the server or database). However, you can't just walk into the kitchen and cook the food yourself. Instead, you interact with the waiter (the API).

  • You give your order (request) to the waiter.
  • The waiter takes the order to the kitchen.
  • The kitchen prepares the food.
  • The waiter brings the food (response) back to your table.

In the digital world, APIs act as this intermediary. They abstract away the complexity of the underlying systems and provide a clean, predictable way for applications to request data or trigger actions.

Types of APIs

While this guide focuses on REST, it is important to understand the broader API landscape. Different paradigms solve different problems.

1. REST (Representational State Transfer)

  • Concept: An architectural style based on standard HTTP protocols. It treats data and functionality as "resources" accessed via URLs.
  • Data Format: Usually JSON, but can be XML, HTML, or plain text.
  • Pros: Highly scalable, cacheable, widely adopted, and easy to understand.
  • Cons: Can lead to over-fetching (getting more data than needed) or under-fetching (needing multiple requests to get all data).

2. SOAP (Simple Object Access Protocol)

  • Concept: A highly structured protocol that uses XML for message formatting. It relies heavily on WSDL (Web Services Description Language).
  • Data Format: Strictly XML.
  • Pros: Extremely rigid and secure. Built-in error handling. ACID compliance. Great for enterprise environments (like banking).
  • Cons: Very verbose, heavy payload, difficult to implement and debug compared to REST.

3. GraphQL

  • Concept: A query language developed by Facebook. Instead of multiple endpoints for different resources, it uses a single endpoint. The client specifies exactly what data it wants.
  • Data Format: JSON.
  • Pros: Solves over-fetching and under-fetching. Highly flexible.
  • Cons: Steeper learning curve. Difficult to cache at the HTTP level. Can allow complex queries that overwhelm the server.

4. gRPC (gRPC Remote Procedure Calls)

  • Concept: Developed by Google, gRPC uses HTTP/2 and Protocol Buffers (Protobufs) to execute procedures on remote servers as if they were local.
  • Data Format: Binary (Protobuf).
  • Pros: Extremely fast and lightweight. Excellent for microservices communication.
  • Cons: Not natively supported by web browsers (requires proxies). Difficult to read payloads without tools.

2. What is REST?

REST stands for Representational State Transfer. It is not a protocol or a standard, but rather an architectural style introduced by Roy Fielding in his 2000 doctoral dissertation.

When an API adheres to the principles of REST, it is referred to as RESTful.

The 6 Guiding Principles of REST

To be considered truly RESTful, an architecture must adhere to six constraints:

1. Client-Server Architecture

The client (user interface/frontend) and the server (data storage/backend) must be completely separated.

  • The client does not concern itself with data storage.
  • The server does not concern itself with the user interface. This separation of concerns allows both components to evolve independently.

2. Statelessness

This is perhaps the most critical principle. Stateless means that the server does not store any state about the client session on the server side.

  • Every request from the client to the server must contain all the information necessary to understand and process the request.
  • The server cannot rely on context from previous requests.
  • Why? It makes servers highly scalable. Any server in a cluster can handle any request because no session data needs to be synchronized between them.

3. Cacheability

Responses from the server must explicitly state whether they are cacheable or non-cacheable.

  • If a response is cacheable, the client (or an intermediary proxy) can reuse that response data for equivalent, subsequent requests.
  • This dramatically improves network efficiency and application performance.

4. Uniform Interface

This simplifies and decouples the architecture. It mandates that there must be a uniform way of interacting with a given server. This principle has four sub-constraints:

  • Identification of resources: Individual resources are identified in requests (e.g., via URIs like /users/123).
  • Manipulation of resources through representations: When a client holds a representation of a resource (like a JSON object), it has enough information to modify or delete that resource on the server.
  • Self-descriptive messages: Each message includes enough information to describe how to process it (e.g., the Content-Type header tells the client it's receiving JSON).
  • HATEOAS (Hypermedia As The Engine Of Application State): The client interacts with the application entirely through hypermedia (links) provided dynamically by the server. (Note: Many modern APIs ignore HATEOAS, reaching only "Level 2" of the Richardson Maturity Model).

5. Layered System

The client cannot ordinarily tell whether it is connected directly to the end server, or to an intermediary along the way (like a load balancer, proxy, or caching layer). This enables load balancing and improves security without the client needing to know.

6. Code on Demand (Optional)

Servers can temporarily extend or customize the functionality of a client by transferring executable code (e.g., JavaScript sent to a web browser). This is the only optional constraint.


3. HTTP Basics

REST APIs are built almost exclusively on top of HTTP (Hypertext Transfer Protocol). To understand REST, you must deeply understand HTTP.

How HTTP Works

HTTP is a request-response protocol. A client sends an HTTP Request, and the server returns an HTTP Response.

HTTP Methods (Verbs)

HTTP defines a set of request methods to indicate the desired action to be performed on a given resource.

Method Description CRUD Mapping Idempotent Safe
GET Retrieves a representation of a resource. Read Yes Yes
POST Submits new data to the server to create a resource. Create No No
PUT Replaces all current representations of the target resource. Update Yes No
PATCH Applies partial modifications to a resource. Update No No
DELETE Deletes the specified resource. Delete Yes No

Concepts:

  • Safe: A safe method does not modify the state of the server. It is read-only (GET).
  • Idempotent: An idempotent method guarantees that making multiple identical requests has the same effect as making a single request.
    • E.g., PUT /users/1 with {name: "Alice"}. Sending this 1 time or 100 times leaves the user named Alice. It is idempotent.
    • E.g., POST /users with {name: "Alice"}. Sending this 1 time creates one Alice. Sending it 100 times creates 100 Alices. It is not idempotent.

HTTP Status Codes

When a server responds, it includes a 3-digit status code that indicates the outcome of the request. They are grouped into 5 classes:

1xx: Informational

  • 100 Continue: The server received the request headers and the client should proceed to send the request body.

2xx: Success

  • 200 OK: Standard response for successful HTTP requests (usually GET or PUT).
  • 201 Created: The request was successful, and a new resource was created (usually POST).
  • 204 No Content: The server successfully processed the request, but is not returning any content (usually DELETE).

3xx: Redirection

  • 301 Moved Permanently: The resource has a new permanent URI.
  • 304 Not Modified: The resource has not been modified since the last request (used for caching).

4xx: Client Errors

  • 400 Bad Request: The server cannot process the request due to a client error (e.g., malformed syntax, invalid data).
  • 401 Unauthorized: The client must authenticate itself to get the requested response. (Actually means "Unauthenticated").
  • 403 Forbidden: The client is authenticated but does not have permission to access the resource.
  • 404 Not Found: The requested resource could not be found.
  • 405 Method Not Allowed: The request method is known by the server but is not supported by the target resource (e.g., trying to POST to a read-only endpoint).
  • 429 Too Many Requests: The user has sent too many requests in a given amount of time (Rate Limiting).

5xx: Server Errors

  • 500 Internal Server Error: A generic error message given when an unexpected condition was encountered on the server.
  • 502 Bad Gateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server.
  • 503 Service Unavailable: The server is currently unable to handle the request (due to overload or maintenance).

HTTP Headers

Headers allow the client and server to pass additional information with an HTTP request or response. They consist of a case-insensitive name followed by a colon (:), then by its value.

Common Request Headers:

  • Accept: What media types the client can understand (e.g., Accept: application/json).
  • Authorization: Credentials for authenticating the client (e.g., Authorization: Bearer <token>).
  • Content-Type: The media type of the body of the request (e.g., Content-Type: application/json).
  • User-Agent: Information about the client application (browser, OS, etc.).

Common Response Headers:

  • Content-Type: The media type of the resource returned.
  • Location: Used in redirections or when a new resource is created to provide the URI of the new resource.
  • Cache-Control: Directives for caching mechanisms in both requests and responses.

4. RESTful Design

Designing a good API is like designing a good UI for developers. It should be intuitive, predictable, and consistent.

Resource-based URLs

In REST, everything is a resource. URLs (Uniform Resource Locators) should identify these resources.

Rule 1: Use Nouns, Not Verbs URLs should represent the entity you are interacting with, not the action you are taking. The action is determined by the HTTP Method.

  • ❌ BAD: /getAllUsers
  • ❌ BAD: /createUser
  • ✅ GOOD: /users (GET to fetch all, POST to create)

Rule 2: Use Plural Nouns Stick to plural nouns for consistency.

  • ❌ BAD: /user or /user/123
  • ✅ GOOD: /users or /users/123

Rule 3: Use Hierarchical Structure for Relations If a resource belongs to another resource, represent that relationship in the URL path.

  • ✅ GOOD: /users/123/posts (Gets all posts belonging to user 123)
  • ✅ GOOD: /users/123/posts/456 (Gets post 456 belonging to user 123)

Note: Don't nest too deeply. A general rule of thumb is no more than 3 levels deep. If you need /users/1/posts/2/comments/3, it's often better to just use /comments/3 if the comment ID is globally unique.

Naming Conventions

  • Use lowercase letters: URIs should be lower case.
  • Use hyphens (-) to separate words: This improves readability. Do not use underscores (_) or camelCase in URLs.
    • ❌ BAD: /api/user_profiles
    • ❌ BAD: /api/userProfiles
    • ✅ GOOD: /api/user-profiles
  • Do not use trailing slashes: /users and /users/ can technically be treated as different resources, causing confusion. Stick to /users.

Versioning

APIs evolve. If you change the structure of a response or require a new mandatory parameter, you might break existing clients. Therefore, you must version your API.

Method 1: URI Versioning (Most Common & Recommended) Place the version number in the URL path.

  • https://api.example.com/v1/users
  • https://api.example.com/v2/users

Method 2: Query Parameter Versioning

  • https://api.example.com/users?version=1

Method 3: Header Versioning (Content Negotiation) Using custom headers or the Accept header.

  • Accept: application/vnd.example.v1+json

URI versioning is generally preferred because it is explicit, easy to test in a browser, and easy to cache.


5. Request and Response Structure

JSON Format

JSON (JavaScript Object Notation) is the de-facto standard for REST API payloads. It is lightweight, readable by humans, and easily parsed by machines.

Example JSON Object:

{
  "id": 123,
  "first_name": "John",
  "last_name": "Doe",
  "is_active": true,
  "roles": ["admin", "editor"],
  "address": {
    "city": "New York",
    "zip": "10001"
  }
}

Best Practice: Pick a casing convention for JSON keys and stick to it globally. snake_case or camelCase are the most common. Do not mix them.

Parameters

Clients pass data to the server in a few different ways.

1. Path Parameters

Used to identify a specific resource. They are part of the URL path.

  • URL: /users/{id}
  • Example: /users/84 (Here, 84 is the path parameter)

2. Query Parameters

Used to sort, filter, or paginate a collection of resources. They are appended to the URL after a question mark ?, separated by ampersands &.

  • URL: /users?role=admin&status=active
  • Example: /posts?sort_by=date&order=desc

3. Request Body (Payload)

Used to send a large amount of data or complex data structures, typically with POST, PUT, or PATCH requests. Sent in the body of the HTTP request, formatted as JSON.

// POST /users
{
  "email": "jane@example.com",
  "password": "securepassword123"
}

6. CRUD Operations

CRUD stands for Create, Read, Update, Delete. These are the four basic operations of persistent storage, and they map perfectly to HTTP methods.

Let's look at how CRUD maps to an articles resource.

1. Create (POST)

  • Endpoint: POST /articles
  • Request Body:
    {
      "title": "My First Article",
      "content": "Hello world!"
    }
  • Response: 201 Created
    {
      "id": 1,
      "title": "My First Article",
      "content": "Hello world!",
      "created_at": "2024-05-05T10:00:00Z"
    }
  • Note: The response often returns the newly created object, including its database-generated ID.

2. Read (GET)

Read a Collection:

  • Endpoint: GET /articles
  • Response: 200 OK
    [
      { "id": 1, "title": "My First Article" },
      { "id": 2, "title": "My Second Article" }
    ]

Read a Single Resource:

  • Endpoint: GET /articles/1
  • Response: 200 OK
    {
      "id": 1,
      "title": "My First Article",
      "content": "Hello world!"
    }
  • If article 99 does not exist, the response is 404 Not Found.

3. Update (PUT vs PATCH)

PUT (Full Update): PUT replaces the entire resource. If you omit a field, it should theoretically be set to null or its default value.

  • Endpoint: PUT /articles/1
  • Request Body:
    {
      "title": "My First Article (Updated)",
      "content": "Hello world! I updated this."
    }
  • Response: 200 OK

PATCH (Partial Update): PATCH is used when you only want to update specific fields without affecting the rest.

  • Endpoint: PATCH /articles/1
  • Request Body:
    {
      "title": "Only updating the title"
    }
  • Response: 200 OK

4. Delete (DELETE)

  • Endpoint: DELETE /articles/1
  • Response: 204 No Content (Empty body, just status code).
  • If called again, it should return 404 Not Found because the resource is gone.

7. Authentication and Authorization

It is crucial to understand the difference between these two:

  • Authentication (AuthN): Proving who you are. (Logging in).
  • Authorization (AuthZ): Proving what you are allowed to do. (Permissions/Roles).

Because REST is stateless, every single request that requires protection must contain authentication credentials. The server cannot "remember" that you logged in on the previous request.

Common Methods

1. API Keys

A simple string generated by the server and given to the client. The client passes it in a header or query parameter.

  • Header: x-api-key: a1b2c3d4e5
  • Use case: Server-to-server communication or public API access tracking. Not secure enough for user logins.

2. Basic Authentication

The client sends the username and password encoded in Base64 in the Authorization header.

  • Header: Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
  • Security: Highly insecure unless sent over HTTPS, as Base64 is trivially decoded.

3. Bearer Tokens (JWT)

The most common modern approach for web/mobile apps.

  1. Client logs in (POST /login) with credentials.
  2. Server verifies and returns a Token (often a JSON Web Token or JWT).
  3. Client stores the token and sends it with every subsequent request.
  • Header: Authorization: Bearer eyJhbGciOiJIUzI1...
  • Use case: User sessions, stateless authentication. JWTs can contain embedded data (like user ID and roles) so the server doesn't have to query the database to verify the token.

4. OAuth 2.0

An industry-standard protocol for authorization. It allows a user to grant a third-party application access to their data without giving them their password (e.g., "Log in with Google" or "Log in with GitHub").


8. Error Handling

A well-designed API must fail gracefully and provide helpful error messages. Simply returning a 500 Internal Server Error with an HTML stack trace is a terrible developer experience.

Standardized Error Responses

Always return a consistent JSON structure for errors. A common approach (inspired by RFC 7807 - Problem Details for HTTP APIs) looks like this:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request payload contains invalid data.",
    "status": 400,
    "details": [
      {
        "field": "email",
        "issue": "Must be a valid email address."
      },
      {
        "field": "password",
        "issue": "Password must be at least 8 characters long."
      }
    ]
  }
}

Key Components of a Good Error:

  1. Appropriate HTTP Status Code: (e.g., 400 instead of 200 with an error message inside).
  2. Internal Error Code: A string or number specific to your application (e.g., USER_NOT_FOUND) so clients can write logic against it.
  3. Human-readable Message: A clear explanation of what went wrong.
  4. Details (Optional): Specific validation errors or contextual data.

9. Pagination, Filtering, Sorting

When a client requests a collection (GET /users), returning 1,000,000 records at once will crash the server, exhaust bandwidth, and freeze the client. You must implement pagination.

Pagination

1. Offset/Limit Pagination (Most Common) Uses limit (how many items to return) and offset (how many items to skip).

  • Request: GET /users?limit=20&offset=40 (Gets users 41-60)
  • Pros: Easy to implement with SQL databases (LIMIT 20 OFFSET 40), allows jumping to a specific page.
  • Cons: Performance degrades as offset gets very large. Data can drift (if an item is added to page 1 while you are viewing page 2, an item might get pushed to page 2 and you see it twice).

2. Cursor-based Pagination Instead of an offset, you pass a "cursor" (often the ID of the last item received).

  • Request: GET /users?limit=20&after=user_id_892
  • Pros: Highly performant even on massive datasets. No data drift issues. Used by infinite-scroll UIs (like Twitter/Facebook).
  • Cons: Cannot easily jump to "Page 15". Only supports "Next" and "Previous".

Response Structure for Pagination: Always include metadata in paginated responses.

{
  "data": [ ...array of users... ],
  "meta": {
    "total_records": 500,
    "current_page": 3,
    "total_pages": 25,
    "next_url": "/users?limit=20&offset=60",
    "prev_url": "/users?limit=20&offset=20"
  }
}

Filtering

Use query parameters to filter datasets.

  • Exact match: GET /users?status=active
  • Multiple values: GET /users?role=admin,editor (or ?role=admin&role=editor)
  • Advanced Operators: Often brackets are used.
    • GET /products?price[gte]=10&price[lte]=50 (Price greater than or equal to 10, less than or equal to 50).

Sorting

Allow clients to specify sort fields and directions.

  • Single sort: GET /users?sort=created_at
  • Sort direction: GET /users?sort=-created_at (The minus sign indicates descending order).
  • Multiple sort: GET /users?sort=last_name,-created_at (Sort by last name ASC, then created_at DESC).

10. Rate Limiting

Rate limiting restricts how many requests a client can make within a specific time window. This protects your API from:

  • Brute force attacks (e.g., guessing passwords).
  • Denial of Service (DoS) attacks.
  • Accidental infinite loops in client code.
  • Resource exhaustion (saving server costs).

How it works

When a client exceeds the limit, the server responds with: Status Code: 429 Too Many Requests

The server should also return standard HTTP headers communicating the limits to the client:

  • X-RateLimit-Limit: The maximum number of requests allowed in the time window.
  • X-RateLimit-Remaining: How many requests the client has left in the current window.
  • X-RateLimit-Reset: The Unix timestamp when the limit window will reset.
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1620230400

{
  "error": "Rate limit exceeded. Try again in 5 minutes."
}

11. Security Best Practices

Building a functional API is only half the job; making it secure is paramount.

  1. Always Use HTTPS: Never serve an API over plain HTTP. HTTPS encrypts the traffic in transit, preventing Man-in-the-Middle (MitM) attacks from stealing tokens or passwords.
  2. Input Validation: Never trust client data. Validate all path parameters, query parameters, and JSON payload fields against strict schemas before processing them. (e.g., ensure an email is actually formatted as an email).
  3. Authentication & Authorization: Enforce strict access control. Ensure a user can only access their own resources (Prevent IDOR - Insecure Direct Object Reference).
    • Example of IDOR: User A logged in, calls GET /users/5/invoices. If the backend doesn't check if User A actually owns User 5, it's a critical vulnerability.
  4. CORS (Cross-Origin Resource Sharing): If your API is consumed by web browsers on different domains, configure CORS properly. Do not use Access-Control-Allow-Origin: * for authenticated routes.
  5. Hide Sensitive Data: Ensure passwords, internal IDs, and security tokens are never accidentally returned in a GET response.
  6. Limit Payload Size: Prevent attacks that try to crash the server by sending huge JSON bodies. Set a strict limit (e.g., 2MB) on incoming request bodies.

12. Testing APIs

Testing ensures your API behaves as expected and prevents regressions when you add new features.

Tools for Manual Testing

  • Postman: The most popular GUI tool for constructing HTTP requests, saving them in collections, and viewing responses.
  • Insomnia: A lightweight, excellent alternative to Postman.
  • cURL: A command-line tool. Great for quick checks.
    curl -X POST https://api.example.com/login \
         -H "Content-Type: application/json" \
         -d '{"username":"admin","password":"password123"}'

Automated Testing

  • Unit Testing: Testing individual functions or controllers in isolation (mocking the database).
  • Integration Testing: Booting up the application, connecting to a test database, and making actual HTTP requests to the endpoints to verify the entire flow (routing -> controller -> database -> response). Tools like Supertest (for Node.js) or Pytest (for Python) are standard.

13. Documentation

An API without documentation is useless to developers. Good documentation should include:

  • Base URLs.
  • Authentication mechanisms.
  • All available endpoints (Methods and Paths).
  • Expected request parameters (Headers, Query, Body schemas).
  • Example request payloads.
  • Expected response payloads for success and error scenarios.

OpenAPI Specification (Swagger)

The industry standard for documenting REST APIs is the OpenAPI Specification (formerly known as Swagger). It is a standard JSON or YAML format that describes your API.

Tools like Swagger UI or Redoc can take an OpenAPI YAML file and generate beautiful, interactive HTML documentation pages where developers can actually "Try it out" directly from the browser.


14. Best Practices Summary

To ensure your API is world-class, keep this checklist in mind:

  1. Be Consistent: If you return { "data": [...] } for one collection, do it for all collections.
  2. Use Plural Nouns: /orders, not /order.
  3. Use Standard Status Codes: Don't return 200 OK with an error message inside. Use 4xx and 5xx appropriately.
  4. Nest logically: /users/123/orders.
  5. Provide Meta-data: For pagination, rate limits, etc.
  6. Version from Day 1: Start with /v1/.
  7. Accept and Respond with JSON: Enforce Content-Type: application/json.
  8. Keep endpoints small and focused: Do not create a /doEverything endpoint.

15. Common Mistakes to Avoid

  • Using GET for state modifications: GET requests should NEVER delete, create, or update data. Web crawlers (like Googlebot) visit GET links automatically. If a link deletes data, a bot could wipe your database.
  • Leaking abstraction details: Don't return database-specific errors (e.g., SQL syntax error near...). This exposes your tech stack to attackers. Return generic validation or server errors.
  • Ignoring caching: Failing to implement ETag or Cache-Control headers for resources that rarely change leads to unnecessary database load.
  • Chatty APIs: Designing an architecture where the client must make 15 separate API calls to load a single page. Try to strike a balance between granular REST resources and useful composite endpoints if needed.

16. Real-world Examples

Let's look at a practical implementation of a REST API using Node.js and Express.js.

Example 1: A Simple CRUD API for "Books"

const express = require('express');
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies

// Mock Database
let books = [
  { id: 1, title: '1984', author: 'George Orwell' },
  { id: 2, title: 'Brave New World', author: 'Aldous Huxley' }
];
let nextId = 3;

// 1. GET /books - Retrieve all books
app.get('/api/v1/books', (req, res) => {
  res.status(200).json({ data: books });
});

// 2. GET /books/:id - Retrieve a single book
app.get('/api/v1/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const book = books.find(b => b.id === bookId);
  
  if (!book) {
    return res.status(404).json({ error: { message: "Book not found" } });
  }
  
  res.status(200).json({ data: book });
});

// 3. POST /books - Create a new book
app.post('/api/v1/books', (req, res) => {
  const { title, author } = req.body;
  
  // Basic Validation
  if (!title || !author) {
    return res.status(400).json({ 
      error: { message: "Title and author are required." } 
    });
  }
  
  const newBook = { id: nextId++, title, author };
  books.push(newBook);
  
  res.status(201).json({ data: newBook });
});

// 4. PUT /books/:id - Update a book
app.put('/api/v1/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const { title, author } = req.body;
  
  const bookIndex = books.findIndex(b => b.id === bookId);
  if (bookIndex === -1) {
    return res.status(404).json({ error: { message: "Book not found" } });
  }
  
  // Update
  books[bookIndex] = { id: bookId, title, author };
  res.status(200).json({ data: books[bookIndex] });
});

// 5. DELETE /books/:id - Delete a book
app.delete('/api/v1/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === bookId);
  
  if (bookIndex === -1) {
    return res.status(404).json({ error: { message: "Book not found" } });
  }
  
  books.splice(bookIndex, 1);
  res.status(204).send(); // No content response
});

app.listen(3000, () => console.log('Server running on port 3000'));

Example 2: Client Consumption (Frontend)

How a frontend application (using JavaScript fetch) would interact with this API.

// Fetching all books
async function getBooks() {
  try {
    const response = await fetch('http://localhost:3000/api/v1/books');
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    
    const result = await response.json();
    console.log("Books list:", result.data);
  } catch (error) {
    console.error("Failed to fetch books:", error);
  }
}

// Creating a new book
async function createBook(title, author) {
  try {
    const response = await fetch('http://localhost:3000/api/v1/books', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ title, author })
    });
    
    const result = await response.json();
    
    if (response.status === 201) {
      console.log("Book created successfully:", result.data);
    } else {
      console.error("Validation failed:", result.error.message);
    }
  } catch (error) {
    console.error("Network error:", error);
  }
}

Conclusion

Mastering REST API design is an essential skill for modern software development. By adhering to standard HTTP conventions, organizing resources logically, implementing robust error handling, and prioritizing security, you can build APIs that are highly scalable, easily maintainable, and a joy for other developers to consume.

Remember that while REST provides a solid architectural foundation, practical implementation often requires pragmatic trade-offs. Always prioritize clarity, consistency, and the developer experience of the clients consuming your API.