Skip to main content

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.

python
@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:

  1. Generated Client Code: When tools generate client code from your API schema, they use operation IDs as function names
  2. API Documentation: Makes your API documentation more readable with meaningful names
  3. Logging and Monitoring: Easier to track specific endpoints in logs and monitoring tools
  4. 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:

python
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:

python
# 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:

python
# 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:

python
# 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:

python
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:

json
{
"/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
# 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:

python
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

Exercises

  1. Update an existing FastAPI project to include operation IDs for all endpoints
  2. Create a helper function that automatically generates consistent operation IDs based on your API's path structure
  3. Generate a client library from your API schema and observe how operation IDs affect the generated code
  4. 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! :)