RESTful API Design Principles

Building Well-Structured, Resource-Oriented Web APIs

Introduction to REST

REST (Representational State Transfer) is an architectural style for designing networked applications, first introduced by Roy Fielding in his 2000 doctoral dissertation. It has become the dominant pattern for building web APIs due to its simplicity, scalability, and alignment with the web's underlying HTTP protocol.

Unlike other API paradigms that focus on actions or procedures, REST is fundamentally resource-oriented. It treats everything as a resource that can be created, read, updated, or deleted using standard HTTP methods. This approach creates intuitive, predictable APIs that leverage the existing capabilities of HTTP.

mindmap root((RESTful API
Principles)) Resource-Oriented Everything is a resource Resources identified by URIs Resources have multiple representations Statelessness Each request contains all needed information No client context stored on server Authentication is per-request Uniform Interface Consistent resource identifiers Standard HTTP methods Self-descriptive messages HATEOAS (Hypermedia) Cacheability Responses explicitly mark themselves as cacheable Improves scalability and performance Layered System Client cannot tell if connected directly to end server Enables load balancing and security policies Code-On-Demand optional Server can extend client functionality

The Library Catalog Analogy

A helpful way to understand RESTful API design is to compare it to a library catalog system:

Resource-Oriented Design

Identifying Resources

The first step in RESTful API design is identifying the resources your API will expose. Resources are the nouns in your system—the entities users need to interact with.

graph TD A[E-commerce Platform Resources] A --> B[Products] A --> C[Orders] A --> D[Customers] A --> E[Reviews] A --> F[Categories] A --> G[Shopping Carts] B --- B1[Individual Product] C --- C1[Individual Order] D --- D1[Individual Customer] E --- E1[Individual Review] F --- F1[Individual Category] G --- G1[Individual Cart]

Resources often map to domain objects in your system, but they can also represent:

Designing Resource URIs

URIs (Uniform Resource Identifiers) provide the addresses for your resources. Well-designed URIs are:

URI Design Best Practices

Example URI Patterns

Resource Collection URI Individual URI
Products /products /products/42
Users /users /users/john.doe
Orders /orders /orders/ORD-2023-1234
User's Orders /users/john.doe/orders /users/john.doe/orders/ORD-2023-1234
Order Items /orders/ORD-2023-1234/items /orders/ORD-2023-1234/items/5

HTTP Methods and CRUD Operations

RESTful APIs use standard HTTP methods to perform CRUD (Create, Read, Update, Delete) operations on resources:

flowchart LR A[HTTP Methods] A -->|Create| B[POST] A -->|Read| C[GET] A -->|Update| D[PUT / PATCH] A -->|Delete| E[DELETE] style A fill:#f9f,stroke:#333,stroke-width:2px
Operation HTTP Method URI Pattern Description
List Collection GET /resources Retrieve all resources (usually paginated)
Get Single Item GET /resources/{id} Retrieve a specific resource by ID
Create Item POST /resources Create a new resource in the collection
Update Item (Full) PUT /resources/{id} Replace a resource entirely
Update Item (Partial) PATCH /resources/{id} Update specific fields of a resource
Delete Item DELETE /resources/{id} Remove a resource

Example CRUD Operations for a Products API

Create a Product (POST)


POST /api/products HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJ...

{
  "name": "Wireless Headphones",
  "price": 129.99,
  "description": "Premium noise-cancelling wireless headphones",
  "category": "electronics",
  "in_stock": true
}

// Response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/products/42

{
  "id": 42,
  "name": "Wireless Headphones",
  "price": 129.99,
  "description": "Premium noise-cancelling wireless headphones",
  "category": "electronics",
  "in_stock": true,
  "created_at": "2023-08-15T10:30:00Z"
}
        

Get All Products (GET)


GET /api/products?category=electronics&sort=price HTTP/1.1
Host: example.com
Accept: application/json

// Response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600

{
  "products": [
    {
      "id": 42,
      "name": "Wireless Headphones",
      "price": 129.99,
      "category": "electronics",
      "in_stock": true
    },
    {
      "id": 17,
      "name": "Bluetooth Speaker",
      "price": 79.99,
      "category": "electronics",
      "in_stock": true
    }
  ],
  "total": 2,
  "page": 1,
  "limit": 10
}
        

Get a Specific Product (GET)


GET /api/products/42 HTTP/1.1
Host: example.com
Accept: application/json

// Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=3600

{
  "id": 42,
  "name": "Wireless Headphones",
  "price": 129.99,
  "description": "Premium noise-cancelling wireless headphones",
  "category": "electronics",
  "in_stock": true,
  "created_at": "2023-08-15T10:30:00Z",
  "updated_at": "2023-08-15T10:30:00Z"
}
        

Update a Product (PUT)


PUT /api/products/42 HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Authorization: Bearer eyJhbGciOiJ...

{
  "name": "Wireless Headphones Pro",
  "price": 149.99,
  "description": "Premium noise-cancelling wireless headphones with extended battery life",
  "category": "electronics",
  "in_stock": true
}

// Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "4d2a46c69fde6fcd2810cd7054b42fba98e91de2"

{
  "id": 42,
  "name": "Wireless Headphones Pro",
  "price": 149.99,
  "description": "Premium noise-cancelling wireless headphones with extended battery life",
  "category": "electronics",
  "in_stock": true,
  "created_at": "2023-08-15T10:30:00Z",
  "updated_at": "2023-08-15T11:15:00Z"
}
        

Update a Product Partially (PATCH)


PATCH /api/products/42 HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJ...

{
  "price": 139.99,
  "in_stock": false
}

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 42,
  "name": "Wireless Headphones Pro",
  "price": 139.99,
  "description": "Premium noise-cancelling wireless headphones with extended battery life",
  "category": "electronics",
  "in_stock": false,
  "created_at": "2023-08-15T10:30:00Z",
  "updated_at": "2023-08-15T11:30:00Z"
}
        

Delete a Product (DELETE)


DELETE /api/products/42 HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJ...

// Response
HTTP/1.1 204 No Content
        

Relationship Handling

Most applications have resources that are related to each other. RESTful APIs need effective ways to express and manipulate these relationships.

Types of Relationships

Nested Resources vs. Linking

There are two main approaches to handling relationships:

Nested Resources Approach


// Get all orders for a specific user
GET /api/users/123/orders

// Get a specific order for a user
GET /api/users/123/orders/456

// Add a new order for a user
POST /api/users/123/orders
        

Advantages:

Disadvantages:

Linking Approach


// User representation includes links to related resources
GET /api/users/123

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "links": {
    "orders": "/api/orders?user_id=123",
    "profile": "/api/profiles/123"
  }
}

// Orders can be accessed directly with filtering
GET /api/orders?user_id=123
        

Advantages:

Disadvantages:

Best Practices for Resource Relationships

HATEOAS and API Evolution

HATEOAS (Hypermedia as the Engine of Application State) is a constraint of the REST application architecture that distinguishes it from other network application architectures. It's often one of the most overlooked aspects of true RESTful design.

Understanding HATEOAS

The core principle is that a client interacts with a REST API entirely through the hypermedia provided dynamically by the server. In other words, the API responses include links that tell the client what it can do next.

Think of it like browsing a website: you don't need to know all the URLs in advance—you follow links from page to page. Similarly, a HATEOAS-compliant API provides "links" that guide the client through the available actions.

Example: Without HATEOAS


GET /api/orders/12345 HTTP/1.1
Host: example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "12345",
  "total": 99.99,
  "status": "shipped",
  "customer_id": "789"
}
        

The client needs prior knowledge about what endpoints are available for further actions.

Example: With HATEOAS


GET /api/orders/12345 HTTP/1.1
Host: example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "12345",
  "total": 99.99,
  "status": "shipped",
  "customer_id": "789",
  "links": [
    { "rel": "self", "href": "/api/orders/12345", "method": "GET" },
    { "rel": "customer", "href": "/api/customers/789", "method": "GET" },
    { "rel": "items", "href": "/api/orders/12345/items", "method": "GET" },
    { "rel": "cancel", "href": "/api/orders/12345/cancel", "method": "POST" },
    { "rel": "return", "href": "/api/orders/12345/return", "method": "POST" }
  ]
}
        

Now the client can discover what actions are possible based on the current state of the resource.

Benefits of HATEOAS

Implementing HATEOAS

Several formats exist for implementing HATEOAS:

HAL (Hypertext Application Language)


{
  "id": "12345",
  "total": 99.99,
  "status": "shipped",
  "_links": {
    "self": { "href": "/api/orders/12345" },
    "customer": { "href": "/api/customers/789" },
    "items": { "href": "/api/orders/12345/items" }
  }
}
        

JSON:API


{
  "data": {
    "type": "orders",
    "id": "12345",
    "attributes": {
      "total": 99.99,
      "status": "shipped"
    },
    "relationships": {
      "customer": {
        "links": {
          "related": "/api/customers/789"
        }
      },
      "items": {
        "links": {
          "related": "/api/orders/12345/items"
        }
      }
    },
    "links": {
      "self": "/api/orders/12345"
    }
  }
}
        

Custom Link Structure


{
  "id": "12345",
  "total": 99.99,
  "status": "shipped",
  "links": [
    { "rel": "self", "href": "/api/orders/12345", "method": "GET" },
    { "rel": "cancel", "href": "/api/orders/12345/cancel", "method": "POST", 
      "available": true, "description": "Cancel this order" }
  ]
}
        

API Evolution Strategies

HATEOAS is one approach to API evolution, but there are several strategies:

Versioning

Compatibility Guidelines

Content Negotiation

Content negotiation is the process of selecting the best representation of a resource when multiple representations are available. This allows clients to request data in their preferred format.

Format Negotiation with Accept Header


// Client requests JSON
GET /api/products/123 HTTP/1.1
Host: example.com
Accept: application/json

// Server responds with JSON
HTTP/1.1 200 OK
Content-Type: application/json

{ "id": 123, "name": "Product Name", "price": 99.99 }

// Client requests XML
GET /api/products/123 HTTP/1.1
Host: example.com
Accept: application/xml

// Server responds with XML
HTTP/1.1 200 OK
Content-Type: application/xml

<product>
  <id>123</id>
  <name>Product Name</name>
  <price>99.99</price>
</product>
        

Language Negotiation with Accept-Language


// Client requests French
GET /api/products/123 HTTP/1.1
Host: example.com
Accept: application/json
Accept-Language: fr-FR,fr;q=0.9,en;q=0.8

// Server responds with French content
HTTP/1.1 200 OK
Content-Type: application/json
Content-Language: fr-FR

{ "id": 123, "nom": "Nom du produit", "prix": 99.99 }
        

Implementing Content Negotiation

Node.js (Express)


app.get('/api/products/:id', (req, res) => {
  const productId = req.params.id;
  const product = getProductById(productId);
  
  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }
  
  // Content negotiation based on Accept header
  res.format({
    'application/json': () => {
      res.json(product);
    },
    'application/xml': () => {
      const xml = convertToXml(product);
      res.type('application/xml').send(xml);
    },
    default: () => {
      // Default to JSON if no match
      res.json(product);
    }
  });
});
        

Python (Flask)


from flask import request, jsonify, Response
import dicttoxml

@app.route('/api/products/')
def get_product(product_id):
    product = get_product_by_id(product_id)
    
    if not product:
        return jsonify({"error": "Product not found"}), 404
    
    # Check Accept header
    best_match = request.accept_mimetypes.best_match(['application/json', 'application/xml'])
    
    if best_match == 'application/xml':
        xml = dicttoxml.dicttoxml(product)
        return Response(xml, mimetype='application/xml')
    else:
        return jsonify(product)
        

PHP


<?php
// Parse Accept header
$accept = $_SERVER['HTTP_ACCEPT'] ?? 'application/json';

// Get product
$productId = // extract from URL
$product = getProductById($productId);

if (!$product) {
    header('Content-Type: application/json');
    http_response_code(404);
    echo json_encode(['error' => 'Product not found']);
    exit;
}

// Content negotiation
if (strpos($accept, 'application/xml') !== false) {
    // Respond with XML
    header('Content-Type: application/xml');
    
    $xml = new SimpleXMLElement('<product/>');
    foreach ($product as $key => $value) {
        $xml->addChild($key, $value);
    }
    
    echo $xml->asXML();
} else {
    // Default to JSON
    header('Content-Type: application/json');
    echo json_encode($product);
}
?>
        

Common REST API Patterns

Pagination

Pagination is essential when dealing with large collections of resources.

Offset-Based Pagination


GET /api/products?page=2&limit=20 HTTP/1.1
Host: example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "products": [...],
  "pagination": {
    "total_items": 547,
    "total_pages": 28,
    "current_page": 2,
    "items_per_page": 20,
    "next_page": "/api/products?page=3&limit=20",
    "prev_page": "/api/products?page=1&limit=20"
  }
}
        

Cursor-Based Pagination


GET /api/products?limit=20&after=eyJpZCI6MTIzfQ== HTTP/1.1
Host: example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "products": [...],
  "pagination": {
    "limit": 20,
    "after": "eyJpZCI6MTQ1fQ==",
    "next": "/api/products?limit=20&after=eyJpZCI6MTQ1fQ=="
  }
}
        

Filtering

Filtering allows clients to request subsets of resources.

Simple Field Filtering


GET /api/products?category=electronics&in_stock=true&min_price=100&max_price=500 HTTP/1.1
Host: example.com
        

Advanced Filtering


GET /api/products?filter[category]=electronics&filter[price][gt]=100&filter[price][lt]=500 HTTP/1.1
Host: example.com
        

Sorting

Sorting controls the order of returned resources.

Single Field Sorting


GET /api/products?sort=price&order=asc HTTP/1.1
Host: example.com
        

Multi-Field Sorting


GET /api/products?sort=category,-price HTTP/1.1
Host: example.com
        

This sorts first by category (ascending) then by price (descending, indicated by the minus sign).

Field Selection

Field selection lets clients request only the fields they need, reducing payload size.

Including Fields


GET /api/products/123?fields=id,name,price HTTP/1.1
Host: example.com

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "name": "Wireless Headphones",
  "price": 129.99
}
        

Excluding Fields


GET /api/products/123?exclude=description,metadata,related_products HTTP/1.1
Host: example.com
        

Bulk Operations

Bulk operations allow clients to perform actions on multiple resources at once.

Bulk Creation


POST /api/products/bulk HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "products": [
    { "name": "Product 1", "price": 19.99 },
    { "name": "Product 2", "price": 29.99 },
    { "name": "Product 3", "price": 39.99 }
  ]
}

// Response
HTTP/1.1 201 Created
Content-Type: application/json

{
  "created": 3,
  "products": [
    { "id": 101, "name": "Product 1", "price": 19.99 },
    { "id": 102, "name": "Product 2", "price": 29.99 },
    { "id": 103, "name": "Product 3", "price": 39.99 }
  ]
}
        

Bulk Update


PATCH /api/products HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "updates": [
    { "id": 101, "price": 24.99 },
    { "id": 102, "price": 34.99 }
  ]
}

// Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "updated": 2,
  "products": [
    { "id": 101, "price": 24.99 },
    { "id": 102, "price": 34.99 }
  ]
}
        

Error Handling

Consistent error handling improves API usability.

Basic Error Response


HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "Invalid product data",
  "message": "Price must be greater than zero",
  "code": "INVALID_PRICE"
}
        

Validation Errors


HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": "Validation failed",
  "message": "Multiple validation errors occurred",
  "code": "VALIDATION_ERROR",
  "validation_errors": [
    {
      "field": "name",
      "message": "Name is required",
      "code": "FIELD_REQUIRED"
    },
    {
      "field": "price",
      "message": "Price must be greater than zero",
      "code": "INVALID_VALUE"
    }
  ]
}
        

Security Considerations for REST APIs

Authentication Methods

Authorization

Authorization determines what authenticated users can do:

Common Security Vulnerabilities

Practical Exercise: RESTful API Design

Task: Design a RESTful API for a Library Management System

For this exercise, you'll design a RESTful API for a library management system that handles books, patrons, and loans.

Requirements:

  1. Identify the key resources in the system
  2. Design URI patterns for each resource and their relationships
  3. Specify which HTTP methods apply to each resource
  4. Design the request and response formats, including status codes
  5. Consider pagination, filtering, and sorting
  6. Design at least one example of using HATEOAS in responses

Resources to Consider:

  • Books: Information about books in the library
  • Authors: Information about book authors
  • Patrons: Library members who can borrow books
  • Loans: Records of books borrowed by patrons
  • Reservations: Requests to borrow books that are currently unavailable

Sample Operations:

  • Search for books by title, author, or category
  • Check out a book
  • Return a book
  • Place a reservation for a book
  • List books currently borrowed by a patron
  • List loan history for a book

Deliverable:

Document your API design, including:

  • Resource URIs
  • HTTP methods
  • Request/response formats
  • Status codes
  • Example requests and responses for at least three operations

Summary

In this session, we've explored the principles of RESTful API design:

Understanding these principles will help you design APIs that are:

In our next session, we'll explore API best practices in more detail, including documentation, versioning, and performance optimization.

Additional Resources