REST API Maturity Levels: 0 to 3
Not all APIs that claim to be RESTful are actually following the best practices and principles of REST architecture
REST API
A REST API (Representational State Transfer Application Programming Interface) is an architectural style for designing networked applications. It's based on a set of principles that define how resources are defined and addressed over the web, allowing clients to interact with server resources.
Key principles of a RESTful API include:
Resources: Everything in a REST API is a resource, which can be a physical object or a logical entity. Resources are identified by unique URIs (Uniform Resource Identifiers).
HTTP Methods: RESTful APIs utilize standard HTTP methods (GET, POST, PUT, DELETE, PATCH) to perform CRUD (Create, Read, Update, Delete) operations on resources. Each method has a specific meaning:
GET: Retrieve a resource or a collection of resources.
POST: Create a new resource.
PUT: Update a resource or create if it doesn’t exist.
DELETE: Remove a resource.
PATCH: Partially update a resource.
Statelessness: Each request from the client to the server must contain all necessary information to process the request. The server doesn't store any client state between requests. This allows for scalability and simplicity.
Uniform Interface: A uniform interface between clients and servers is maintained through the use of URIs to identify resources, standard HTTP methods for operations, and representation of resources (often in JSON or XML).
Hypermedia as the Engine of Application State (HATEOAS): A constraint where the server provides hypermedia links within the responses to guide clients on the available actions or state transitions.
A REST API provides a standardized way for different systems to communicate over the internet. It allows clients to perform operations on resources exposed by the server through well-defined and predictable interfaces. RESTful APIs are widely used due to their simplicity, scalability, and compatibility with the HTTP protocol. They are the foundation of many modern web services and applications.
Examples of REST API
Example 1: Users Resource
Endpoint:
/users
HTTP Methods:
GET /users
: Retrieve a list of users.POST /users
: Create a new user.GET /users/{id}
: Retrieve details of a specific user.PUT /users/{id}
: Update details of a specific user.DELETE /users/{id}
: Delete a specific user.
Example 2: Articles Resource
Endpoint:
/articles
HTTP Methods:
GET /articles
: Retrieve a list of articles.POST /articles
: Create a new article.GET /articles/{id}
: Retrieve details of a specific article.PUT /articles/{id}
: Update details of a specific article.DELETE /articles/{id}
: Delete a specific article.
Example 3: Products Resource
Endpoint:
/products
HTTP Methods:
GET /products
: Retrieve a list of products.POST /products
: Create a new product.GET /products/{id}
: Retrieve details of a specific product.PUT /products/{id}
: Update details of a specific product.DELETE /products/{id}
: Delete a specific product.
Example 4: Orders Resource
Endpoint:
/orders
HTTP Methods:
GET /orders
: Retrieve a list of orders.POST /orders
: Create a new order.GET /orders/{id}
: Retrieve details of a specific order.PUT /orders/{id}
: Update details of a specific order.DELETE /orders/{id}
: Delete a specific order.
These examples showcase typical RESTful endpoints representing different resources, each supporting standard CRUD operations using HTTP methods like GET, POST, PUT, and DELETE. The endpoints allow clients to interact with these resources to perform actions such as retrieving, creating, updating, and deleting data.
Richardson Maturity Model
The Richardson Maturity Model, introduced by Leonard Richardson, is a model that defines different levels of maturity for RESTful APIs based on their adherence to REST principles. It provides a framework for evaluating how well an API follows RESTful practices.
The model consists of four levels, each representing a different degree of adherence to REST principles:
Level 0: The Swamp of POX (Plain Old XML)
Characteristics:
Use of a single URI and HTTP POST requests for all actions.
No clear distinction between resources.
Lack of proper use of HTTP methods.
Not RESTful; resembles more traditional RPC (Remote Procedure Call) style.
Level 1: Resources
Characteristics:
Introduces the concept of resources.
Resources are identified by unique URIs.
Different actions might be represented by different URIs, but mostly using a single HTTP method (e.g., GET for everything).
Focuses on identification of resources but doesn’t utilize HTTP methods effectively.
Level 2: HTTP Verbs
Characteristics:
Properly utilizes HTTP methods (GET, POST, PUT, DELETE) for CRUD operations.
Each method has a clear purpose: GET for retrieval, POST for creation, PUT for update, DELETE for deletion.
Resources are identified by URIs, and HTTP methods are used more appropriately for different actions.
Level 3: Hypermedia Controls (HATEOAS)
Characteristics:
Utilizes hypermedia links within responses.
Responses contain links that guide clients on available actions or state transitions.
Clients navigate the application state by following links embedded in responses, reducing coupling between client and server.
Significance:
The model helps assess an API’s maturity in adhering to REST principles.
Higher levels of maturity indicate better adherence to RESTful practices.
Higher levels often lead to more flexible, discoverable, and maintainable APIs.
Achieving higher levels might require more complexity in implementation, especially Level 3 (HATEOAS).
The Richardson Maturity Model provides a framework for evaluating and improving the design and implementation of RESTful APIs, encouraging developers to progress towards higher levels of maturity for more robust and scalable APIs.
Examples of REST API Maturity
The following examples are written with FastAPI framework.
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It comes with built-in support for OpenAPI and JSON Schema, automatic generation of documentation (including interactive API docs), and validation.
Example 1: Maturity Level 0
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel
from typing import Annotated
app = FastAPI()
# payload schema
class Payload(BaseModel):
items: str | None = None
users: str | None = None
# Example in-memory data (pretending to be a database)
fake_db = {
"items": [],
"users": []
}
@app.post("/data", status_code=201)
async def create_data(payload: Annotated[
Payload, Body(openapi_examples={
"items": {
"summary": "payload to create an item",
"description": "Create an item.",
"value": {
"items": "item1",
},
},
"users": {
"summary": "payload to create an user",
"description": "Create an user",
"value": {
"users": "user1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"item": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.items:
fake_db["items"].append(payload.items)
return {"resource": "items", "item_created": payload.items}
elif payload.users:
fake_db["users"].append(payload.users)
return {"resource": "users", "user_created": payload.users}
else:
raise HTTPException(status_code=404, detail="Resource not found")
Explanation:
The endpoint
/data
accepts aPOST
request to create a new item in a specified resource.It takes request body as payload data shown in OpenAPI examples.
The
fake_db
dictionary simulates an in-memory database with multiple resources.When a POST request is made to this endpoint with a request body (
items
orusers
) , it adds the item to the corresponding resource in thefake_db
.If the requested resource doesn't exist, it raises an HTTP 404 error.
This example represents a simple way of handling a POST request to create items within different resources using a single endpoint. However, it doesn't strictly follow RESTful practices, as it's combining multiple resources under a single endpoint, which might not be ideal for larger or more complex APIs.
Example 2: Maturity Level 1
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel
from typing import Annotated
app = FastAPI()
class Payload(BaseModel):
items: str | None = None
users: str | None = None
# Example in-memory data (pretending to be a database)
fake_db = {
"items": [],
"users": []
}
@app.post("/items", status_code=201)
async def create_item(payload: Annotated[
Payload, Body(openapi_examples={
"items": {
"summary": "payload to create an item",
"description": "Create an item.",
"value": {
"items": "item1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"item": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.items:
fake_db["items"].append(payload.items)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "items", "item_created": payload.items}
@app.post("/users", status_code=201)
async def create_user(payload: Annotated[
Payload, Body(openapi_examples={
"users": {
"summary": "payload to create an user",
"description": "Create an user",
"value": {
"users": "user1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"user": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.users:
fake_db["users"].append(payload.users)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "users", "user_created": payload.users}
Explanation:
This code defines two separate endpoints:
/items
and/users
.Each endpoint is responsible for creating an item within a specific resource (
items
orusers
).When a
POST
request with request body shown in Open API examples is made to/items
, it adds the item to theitems
resource in thefake_db
.Similarly, when a
POST
request with request body shown in Open API examples is made to/users
, it adds the user to theusers
resource in thefake_db
.The API doesn't strictly follow all RESTful principles, but it starts to organize resources using separate endpoints for each resource, which is a step towards maturity level 1.
This example demonstrates the beginning of resource identification by having separate endpoints for creating items in different resources. However, it still lacks the full utilization of HTTP methods and the complete adherence to RESTful principles found in higher maturity levels of the Richardson Maturity Model.
Example 3: Maturity Level 2
from fastapi import FastAPI, HTTPException, Body
from pydantic import BaseModel
from typing import Annotated
app = FastAPI()
class Payload(BaseModel):
items: str | None = None
users: str | None = None
# Example in-memory data (pretending to be a database)
fake_db = {
"items": [],
"users": []
}
@app.get("/items")
async def get_items():
"""Retrieve a list of items"""
return {"items": fake_db["items"]}
@app.post("/items", status_code=201)
async def create_item(payload: Annotated[
Payload, Body(openapi_examples={
"items": {
"summary": "payload to create an item",
"description": "Create an item.",
"value": {
"items": "item1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"item": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.items:
fake_db["items"].append(payload.items)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "items", "item_created": payload.items}
@app.get("/users")
async def get_users():
"""Retrieve a list of users"""
return {"users": fake_db["users"]}
@app.post("/users", status_code=201)
async def create_user(payload: Annotated[
Payload, Body(openapi_examples={
"users": {
"summary": "payload to create an user",
"description": "Create an user",
"value": {
"users": "user1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"user": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.users:
fake_db["users"].append(payload.users)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "users", "user_created": payload.users}
Explanation:
Each resource (
items
,users
) has its own dedicated endpoints (/items
,/users
) for handling CRUD operations.The
/items
endpoint supports GET (retrieve items) and POST (create items) operations for theitems
resource.Similarly, the
/users
endpoint supports GET (retrieve users) and POST (create users) operations for theusers
resource.This code uses in-memory data (
fake_db
) to simulate storage for items and users.
This example aligns with RESTful principles by providing separate endpoints for each resource and utilizing appropriate HTTP methods (GET and POST) for retrieving and creating items or users within those resources.
Example 4: Maturity Level 3
from fastapi import FastAPI, Response, HTTPException, Body
from pydantic import BaseModel
from typing import Annotated, List
app = FastAPI()
class Payload(BaseModel):
items: str | None = None
users: str | None = None
# Example in-memory data (pretending to be a database)
fake_db = {
"items": [],
"users": []
}
@app.post("/items", status_code=201)
async def create_item(payload: Annotated[
Payload, Body(openapi_examples={
"items": {
"summary": "payload to create an item",
"description": "Create an item.",
"value": {
"items": "item1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"item": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.items:
fake_db["items"].append(payload.items)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "items", "item_created": payload.items}
@app.get("/items", response_model=List[str])
async def get_items(response: Response):
"""Retrieve a list of items with hypermedia links"""
response.headers["Link"] = f"</users>; rel='users'"
return fake_db["items"]
@app.get("/users", response_model=List[str])
async def get_users(response: Response):
"""Retrieve a list of users with hypermedia links"""
response.headers["Link"] = f"</items>; rel='items'"
return fake_db["users"]
@app.post("/users", status_code=201)
async def create_user(payload: Annotated[
Payload, Body(openapi_examples={
"users": {
"summary": "payload to create an user",
"description": "Create an user",
"value": {
"users": "user1",
},
},
"invalid": {
"summary": "Invalid data is rejected with an error",
"description": "Invalid payload",
"value": {
"user": 123
},
},
},
),
],
):
"""Create data for a specified resource"""
if payload.users:
fake_db["users"].append(payload.users)
else:
raise HTTPException(status_code=404, detail="Resource not found")
return {"resource": "users", "user_created": payload.users}
Explanation:
The code defines
/items
and/users
endpoints for managing items and users, respectively.Each GET request to
/items
or/users
includes aLink
header in the response, pointing to the other resource.For example, when you request
/items
, it includes aLink
header pointing to/users
, and vice versa.This simulates the idea of providing hypermedia links in API responses, guiding clients on available transitions to related resources.
While this example demonstrates a basic form of providing hypermedia links in API responses, full HATEOAS implementation involves more complex navigation and providing links for various state transitions, which might not be feasible to achieve completely within the scope of a simple example or without more advanced customizations.
The source code used in this article could be found in Source code.
Summary
Each level of maturity represents a progression towards a more RESTful design. While higher levels offer advantages like better discoverability, reduced coupling, and improved scalability, they also come with increased complexity and might require more careful design and implementation to fully leverage the benefits. Choosing the appropriate level of maturity often depends on the specific requirements and trade-offs of the API being developed.
References
Richardson Maturity Model - Lokesh Gupta
Richardson Maturity Model - wikipedia
Richardson Maturity Model - devopedia
Richardson Maturity Model – RESTful API - geeksforgeeks