← Back to list

How to Implement HATEOAS in FastAPI (And Why Your API Needs It)

Make your REST APIs truly RESTful with hypermedia-driven navigation

Lotfi ben othman · 2025-11-28 07:37 · 73 claps · 3.6 min read
#python #fastapi #python-programming #rest-api #hateoas
Open on Medium ↗
Wiki topics: 💻 · Programming

How to Implement HATEOAS in FastAPI (And Why Your API Needs It)

Make your REST APIs truly RESTful with hypermedia-driven navigation

Most REST APIs return lists, metadata, and maybe some pagination numbers. But when a client wants page 2, what do they actually do?

Option A: Manually construct URLs like /items?page=2&limit=10 and hope your API never changes.

Option B: Follow links your API gives them:

"next": "https://api.example.com/items?page=2&limit=10"

Option B is HATEOAS — and it transforms a brittle client into a resilient one.

In this guide, I’ll show you how to implement clean, reusable HATEOAS pagination in FastAPI using a pattern you can drop into any project.

What Is HATEOAS?

HATEOAS stands for Hypermedia as the Engine of Application State, one of REST’s original constraints. The idea: clients should discover navigation paths from the API responses themselves, not construct URLs by hand.

Example:

{
  "items": [
    {"id": "abc-123", "name": "Product A"},
    {"id": "def-456", "name": "Product B"}
  ],
  "total_count": 150,
  "page": 1,
  "limit": 10,
  "links": {
    "first": "https://api.example.com/products?page=1&limit=10",
    "next": "https://api.example.com/products?page=2&limit=10",
    "last": "https://api.example.com/products?page=15&limit=10"
  }
}

The client doesn’t know or care how URLs are built — they simply follow the links.

Why HATEOAS Matters

1. Clients Become Decoupled

If you change your URL structure, routes, or query parameters, HATEOAS-aware clients keep working.

2. Self-Documenting APIs

Clients instantly know available actions — without reading docs.

3. Filters Are Preserved Automatically

Filtering with: ?category=electronics&status=active shouldn’t disappear the moment you paginate. HATEOAS takes care of it.

4. Frontend Developers Love It

No more string concatenation. No more fragile URL reconstruction.

Implementing HATEOAS in FastAPI

We’ll build a clean, reusable system with:

  1. A Pydantic model for pagination params
  2. A link-builder
  3. A response wrapper
  4. A FastAPI dependency for effortless integration

Step 1: Define Pagination Parameters

from enum import Enum
from pydantic import BaseModel, PositiveInt

class Ordering(str, Enum):
    asc = "asc"
    desc = "desc"

class ListingParams(BaseModel):
    page: PositiveInt = 1
    limit: PositiveInt = 10
    order_by: str = "created_at"
    ordering: Ordering = Ordering.desc

Step 2: Build Pagination Links

from typing import Any
from fastapi import Request

def build_pagination_links(
    request: Request,
    page: int,
    limit: int,
    total_count: int,
) -> dict[str, str]:
    base_url = str(request.url.remove_query_params(["page", "limit"]))

    query_params = dict(request.query_params)
    query_params.pop("page", None)
    query_params.pop("limit", None)

    extra_params = "&".join(f"{k}={v}" for k, v in query_params.items())

    separator = "&" if extra_params else ""
    total_pages = (total_count + limit - 1) // limit if total_count > 0 else 1

    links = {
        "first": f"{base_url}?page=1&limit={limit}{separator}{extra_params}",
        "last": f"{base_url}?page={total_pages}&limit={limit}{separator}{extra_params}",
    }

    if page < total_pages:
        links["next"] = (
            f"{base_url}?page={page + 1}&limit={limit}{separator}{extra_params}"
        )

    if page > 1:
        links["previous"] = (
            f"{base_url}?page={page - 1}&limit={limit}{separator}{extra_params}"
        )

    return links

Step 3: Wrap Everything in a Response Builder

def create_hateoas_response(
    request: Request,
    listing_params: ListingParams,
    items: list[dict[str, Any]],
    total_count: int,
) -> dict[str, Any]:
    links = build_pagination_links(
        request,
        listing_params.page,
        listing_params.limit,
        total_count
    )

  return {
          "items": items,
          "total_count": total_count,
          "page": listing_params.page,
          "limit": listing_params.limit,
          "links": links,
      }

This is the final structure your endpoints will return.

Step 4: Build a FastAPI Dependency

from functools import partial
from fastapi import Depends, Request

def hateoas_dependency(
    # We should inject the request and the listing params
    request: Request,
    listing_params: ListingParams = Depends()
):
    return partial(
        create_hateoas_response,
        request=request,
        listing_params=listing_params
    )

Using partial() pre-configures context—clean and elegant.

Using HATEOAS in Your Endpoints

from fastapi import APIRouter, Depends

router = APIRouter()
@router.get("/products")
async def list_products(
    category_filter: str | None = None,
    status_filter: str | None = None,
    hateoas = Depends(hateoas_dependency),
    use_case: ListProductsUseCase = Depends(get_list_products_usecase),
):
    result = await use_case.execute(
        category=category,
        status=status
    )

    items = [
        {
            "id": str(product.id),
            "name": product.name,
            "category_filter": product.category,
            "status_filter": product.price
        }
        for product in result.items
    ]

    return hateoas(items=items, total_count=result.count)

The endpoint stays beautifully clean.

Example Response

{
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440006",
      "name": "Wireless Headphones",
      "category": "electronics",
      "price": 79.99
    }
  ],
  "total_count": 47,
  "page": 2,
  "limit": 5,
  "links": {
    "first": "...",
    "previous": "...",
    "next": "...",
    "last": "..."
  }
}

All filters from the original request remain intact.

Adding Resource-Level Links (Optional, Powerful)

def add_resource_links(
    request: Request,
    items: list[dict[str, Any]],
    resource_path: str
) -> list[dict[str, Any]]:
    """Add self links to each item."""
    base = str(request.base_url).rstrip("/")

  for item in items:
          item["links"] = {
              "self": f"{base}/{resource_path}/{item['id']}"
          }
      return items

This enables clients to traverse from lists to individual resources.

Is HATEOAS Only for Listing Endpoints?

Not at all. While HATEOAS is commonly showcased through pagination examples, it applies to any resource and any action in your API. Its purpose is to help clients discover what they can do next — without hardcoding URLs or workflow rules.

Here are a few places where HATEOAS becomes even more powerful:

1. Single Resource Responses

You can include links that describe available actions on a resource:

{
  "id": "123",
  "name": "Product A",
  "links": {
    "self": "/products/123",
    "update": "/products/123",
    "delete": "/products/123"
  }
}

2. Workflow or State Transitions

For resources that follow a lifecycle (e.g., draft → review → published), HATEOAS exposes only the valid next actions:

{
  "id": "123",
  "state": "draft",
  "links": {
    "submit_for_review": "/posts/123/submit"
  }
}

3. Navigation Between Related Resources

Useful for nested or linked data:

{
  "id": "888",
  "comment": "Looks good",
  "links": {
    "self": "/tasks/4/comments/888",
    "task": "/tasks/4",
    "project": "/projects/1"
  }
}

One last note, this article uses offset pagination because it’s simple, but it’s not always the best option. For large or fast-changing datasets, cursor or keyset pagination can give you far better performance and consistency. I’ll cover the differences (and how to implement each in FastAPI) in a separate article.


메타데이터
post_id
7be5fac3a37d
slug
how-to-implement-hateoas-in-fastapi-and-why-your-api-needs-it-7be5fac3a37d
url
https://medium.com/@benothman.lotfi/how-to-implement-hateoas-in-fastapi-and-why-your-api-needs-it-7be5fac3a37d
canonical_url
https://medium.com/@benothman.lotfi/how-to-implement-hateoas-in-fastapi-and-why-your-api-needs-it-7be5fac3a37d
author_url
https://medium.com/@benothman.lotfi
status
ok
fetched_at
2026-06-09 15:37:30