FastAPI Operation ID
When building APIs with FastAPI, you'll often need to precisely identify each operation (endpoint) for documentation and client generation purposes. This is where the operation_id
parameter comes in - a powerful but often overlooked feature that can significantly improve your API architecture.
What is an Operation ID?
An operation ID is a unique identifier assigned to each API endpoint (operation) in your FastAPI application. It serves as a distinctive name for the endpoint in the generated OpenAPI schema. Think of it as a special name tag for each route that makes it easy to reference in documentation and generated code.
@app.get("/items/", operation_id="list_all_items")
def get_items():
return {"items": ["item1", "item2"]}
Why Use Operation IDs?
Before diving into how to use operation IDs, let's understand why they're valuable:
- Generated Client Code: When tools generate client code from your API schema, they use operation IDs as function names
- API Documentation: Makes your API documentation more readable with meaningful names
- Logging and Monitoring: Easier to track specific endpoints in logs and monitoring tools
- API Versioning: Helps maintain consistent references to endpoints across versions
How to Set Operation IDs in FastAPI
Setting an operation ID is straightforward with FastAPI's route decorators:
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/", operation_id="list_users")
def get_users():
return {"users": ["user1", "user2"]}
@app.post("/users/", operation_id="create_user")
def create_user(name: str):
return {"message": f"User {name} created"}
In this example, we've assigned the operation IDs list_users
and create_user
to our GET and POST endpoints respectively.
Operation ID Best Practices
To get the most out of operation IDs, follow these best practices:
1. Use Descriptive Names
Choose operation IDs that clearly describe what the endpoint does:
# Good
@app.get("/products/{product_id}", operation_id="get_product_by_id")
# Not as good
@app.get("/products/{product_id}", operation_id="get_prod")
2. Maintain Consistency
Adopt a consistent naming convention for your operation IDs:
# Consistent pattern: <action>_<resource>[_by_<parameter>]
@app.get("/users/", operation_id="list_users")
@app.post("/users/", operation_id="create_user")
@app.get("/users/{user_id}", operation_id="get_user_by_id")
@app.put("/users/{user_id}", operation_id="update_user_by_id")
@app.delete("/users/{user_id}", operation_id="delete_user_by_id")
3. Ensure Uniqueness
Each operation ID must be unique across your entire API:
# Correct - Each has unique operation_id
@app.get("/items/", operation_id="list_items")
@app.get("/products/", operation_id="list_products")
# Incorrect - Duplicate operation_id
@app.get("/items/", operation_id="list_all")
@app.get("/products/", operation_id="list_all") # Duplicate!
Practical Example: E-commerce API with Operation IDs
Let's build a simple e-commerce API using proper operation IDs:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI(title="E-commerce API")
# Models
class Product(BaseModel):
id: int
name: str
price: float
description: Optional[str] = None
# Sample data
products_db = [
Product(id=1, name="Laptop", price=999.99, description="Powerful laptop"),
Product(id=2, name="Smartphone", price=499.99, description="Latest model"),
]
# Product endpoints with operation IDs
@app.get("/products/", response_model=List[Product], operation_id="list_products")
def get_products():
"""Get all products in the store"""
return products_db
@app.get("/products/{product_id}", response_model=Product, operation_id="get_product_by_id")
def get_product(product_id: int):
"""Get a specific product by ID"""
for product in products_db:
if product.id == product_id:
return product
raise HTTPException(status_code=404, detail="Product not found")
@app.post("/products/", response_model=Product, operation_id="create_product")
def create_product(product: Product):
"""Add a new product to the store"""
products_db.append(product)
return product
@app.put("/products/{product_id}", response_model=Product, operation_id="update_product")
def update_product(product_id: int, updated_product: Product):
"""Update an existing product"""
for i, product in enumerate(products_db):
if product.id == product_id:
updated_product.id = product_id # Ensure ID stays the same
products_db[i] = updated_product
return updated_product
raise HTTPException(status_code=404, detail="Product not found")
Impact on OpenAPI Documentation
When you define operation IDs, they appear in your OpenAPI schema and are used by Swagger UI. Let's see how the OpenAPI schema for one of our endpoints would look:
{
"/products/{product_id}": {
"get": {
"operationId": "get_product_by_id",
"summary": "Get a specific product by ID",
"parameters": [
{
"name": "product_id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Product"
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
}
}
Client Code Generation
One of the biggest benefits of operation IDs is how they affect generated client code. When using tools like OpenAPI Generator, your operation IDs become function names:
# Python client generated from our API
client = EcommerceApiClient()
# Using our operation IDs as method names
all_products = client.list_products()
specific_product = client.get_product_by_id(product_id=1)
Without operation IDs, the generated method names would be much less intuitive, often using combinations of HTTP methods and paths.
Auto-generating Operation IDs
If you have a large API and want to ensure consistent operation IDs, you could create a helper function:
def generate_operation_id(endpoint: str, method: str) -> str:
"""Generate a consistent operation ID from endpoint and method"""
method_prefix = {
"get": "get" if "{" in endpoint else "list",
"post": "create",
"put": "update",
"delete": "delete"
}
# Extract resource name from path
parts = [p for p in endpoint.strip("/").split("/") if not p.startswith("{")]
resource = parts[-1] if parts else "root"
# Make singular for get/update/delete with parameter
if method != "post" and "{" in endpoint:
if resource.endswith("s"):
resource = resource[:-1] # Remove trailing 's'
prefix = method_prefix.get(method.lower(), method.lower())
return f"{prefix}_{resource}"
# Usage example:
@app.get("/users/{user_id}", operation_id=generate_operation_id("/users/{user_id}", "get"))
def get_user(user_id: int):
# Will have operation_id="get_user"
pass
Summary
Operation IDs are a crucial but often overlooked part of building robust APIs with FastAPI. By assigning meaningful, unique identifiers to your endpoints, you create:
- More intuitive, auto-generated client code
- Cleaner API documentation
- Better operability with API management tools
- More maintainable codebase over time
When defining your FastAPI routes, take the extra moment to add thoughtful operation IDs - your future self and API consumers will thank you!
Additional Resources
- FastAPI Official Docs on Operation ID
- OpenAPI Specification on operationId
- OpenAPI Generator - Generate client libraries using your API's operation IDs
Exercises
- Update an existing FastAPI project to include operation IDs for all endpoints
- Create a helper function that automatically generates consistent operation IDs based on your API's path structure
- Generate a client library from your API schema and observe how operation IDs affect the generated code
- Define a REST API for a blog with appropriate operation IDs for posts, comments, and users
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)