FastAPI: Map One Function to Multiple Routes
In the rapidly evolving landscape of web development, building robust, efficient, and maintainable APIs is paramount. FastAPI has emerged as a powerhouse for constructing high-performance APIs with Python, lauded for its speed, automatic interactive API documentation based on OpenAPI, and its elegant integration with Python type hints. Developers flock to FastAPI for its developer-friendly syntax and the tangible benefits it brings to projects, from small microservices to large-scale enterprise applications. One particularly powerful feature, often overlooked but incredibly valuable, is the ability to map a single underlying function to multiple distinct routes. This technique significantly enhances code reusability, simplifies maintenance, and ensures consistency across your API, all while keeping your application lean and your development workflow smooth.
This comprehensive guide will delve deep into the mechanics, best practices, and practical applications of mapping one function to multiple routes in FastAPI. We'll explore various scenarios where this approach shines, discuss its implications for API design and OpenAPI documentation, and ultimately demonstrate how it can lead to more elegant and efficient API development. Whether you're a seasoned FastAPI developer looking to refine your api architecture or new to the framework and eager to master its advanced capabilities, understanding this technique will undoubtedly elevate your API building prowess. We will also touch upon how this level of internal api management within a framework complements broader api gateway solutions for comprehensive api lifecycle governance.
The Foundation: Understanding FastAPI's Routing Mechanism
Before we dive into the specifics of mapping one function to multiple routes, it's crucial to solidify our understanding of how FastAPI handles routing in the first place. At its core, a web api is a collection of endpoints, each corresponding to a specific URL path and an HTTP method (GET, POST, PUT, DELETE, etc.). When a client makes a request to a particular URL using a certain HTTP method, the web server needs to know which piece of code, or "route handler function," should process that request.
FastAPI, built upon Starlette, provides an intuitive and Pythonic way to define these routes using decorators. A decorator is a special type of function in Python that can modify or enhance another function. In FastAPI, route decorators like @app.get(), @app.post(), @app.put(), @app.delete(), and @app.patch() are used to associate an HTTP method and a URL path with a Python function.
Consider a simple example:
from fastapi import FastAPI
app = FastAPI()
@app.get("/techblog/en/items/")
async def read_items():
"""
Retrieves a list of all available items.
"""
return {"message": "Here are all your items!"}
@app.post("/techblog/en/items/")
async def create_item(item: dict):
"""
Creates a new item with the provided data.
"""
return {"message": f"Item created: {item}"}
In this snippet, we have two distinct functions: read_items and create_item. Each is decorated with a specific route decorator, @app.get("/techblog/en/items/") and @app.post("/techblog/en/items/") respectively. This clearly tells FastAPI that when a GET request comes to /items/, read_items should be invoked, and when a POST request comes to the same path, create_item should handle it. This explicit, one-to-one (or one-to-HTTP_method-on-path) mapping is the default and most common way to define routes in FastAPI.
One of FastAPI's standout features is its automatic generation of interactive API documentation, powered by OpenAPI (formerly Swagger). When you define routes with type hints and Pydantic models, FastAPI meticulously parses this information to construct a detailed OpenAPI specification. This specification then fuels tools like Swagger UI and ReDoc, providing a comprehensive, browsable interface for your api consumers. Every path, every HTTP method, every parameter, and every response model is beautifully documented, making api consumption a breeze. This deep integration with OpenAPI is a significant reason behind FastAPI's rapid adoption, as it drastically reduces the effort required for api documentation and testing.
While this standard routing mechanism is powerful and sufficient for most scenarios, there are times when an exact one-to-one mapping isn't the most efficient or logical solution. Imagine a situation where you have a core piece of logic that needs to be accessible through slightly different URLs, perhaps for backward compatibility, aliasing, or subtle variations in access patterns. This is where the technique of mapping one function to multiple routes truly shines, allowing for enhanced code reuse without sacrificing the clarity and robustness of your api design. This flexibility is a hallmark of a well-designed api framework, enabling developers to adapt to diverse architectural needs.
The Core Technique: Mapping One Function to Multiple Routes
The essence of mapping a single function to multiple routes in FastAPI lies in applying multiple route decorators to the same function. This might seem counter-intuitive at first, given the typical one-decorator-per-function pattern, but it's a perfectly valid and incredibly useful feature that FastAPI (and Starlette, its underlying framework) supports natively. By doing so, you instruct FastAPI to invoke the same Python function regardless of which of the specified paths is requested, as long as the HTTP method matches.
Method 1: Applying Multiple Decorators to a Single Function
This is the most direct and common way to achieve our goal. You simply stack the route decorators one above the other, directly above the function definition. Each decorator specifies a unique path (and optionally, an HTTP method if using a generic @app.api_route()), and all these paths will point to the same handler function.
Let's illustrate with a clear example:
from fastapi import FastAPI, Depends, HTTPException, status
from typing import Dict, Any
app = FastAPI(
title="Multi-Route Item Management API",
description="An API demonstrating how to map a single function to multiple routes for managing items.",
version="1.0.0"
)
# In-memory database for demonstration
fake_database = {
"item_alpha": {"name": "Alpha Item", "price": 12.00, "description": "The first item in our inventory."},
"item_beta": {"name": "Beta Item", "price": 25.50, "description": "A second, slightly more expensive item."},
"item_gamma": {"name": "Gamma Item", "price": 7.75, "description": "Our most affordable item, great for bulk orders."}
}
async def verify_item_exists(item_id: str) -> Dict[str, Any]:
"""
Dependency to check if an item exists in the database.
Raises HTTPException if not found.
"""
if item_id not in fake_database:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item '{item_id}' not found."
)
return fake_database[item_id]
@app.get("/techblog/en/items/{item_id}", summary="Get a specific item by ID")
@app.get("/techblog/en/products/{item_id}", summary="Retrieve a product by its identifier")
@app.get("/techblog/en/catalog/{item_id}", summary="Access a catalog entry by its unique ID")
async def get_item_detail(
item_id: str,
item_data: Dict[str, Any] = Depends(verify_item_exists)
):
"""
This function serves multiple routes to fetch details of a single item.
It demonstrates how different paths can point to the same underlying logic,
providing flexibility for API consumers or during API evolution.
- **item_id**: The unique identifier of the item.
- Returns detailed information about the item if found.
"""
return {
"item_id": item_id,
"details": item_data,
"source_path": "dynamic_based_on_request" # This would actually be determined by request context if needed
}
@app.delete("/techblog/en/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
@app.delete("/techblog/en/archive/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Admin"])
async def delete_item(
item_id: str,
item_to_delete: Dict[str, Any] = Depends(verify_item_exists) # Ensure item exists before attempting to delete
):
"""
Deletes an item from the inventory. This function handles deletion
from both the primary `/items/{item_id}` endpoint and an administrative
`/archive/items/{item_id}` endpoint, ensuring consistent deletion logic.
- **item_id**: The unique identifier of the item to be deleted.
- Returns a 204 No Content status upon successful deletion.
"""
print(f"Attempting to delete item: {item_id}")
del fake_database[item_id]
return
# Example of a POST request also leveraging this pattern
@app.post("/techblog/en/add_new_item", status_code=status.HTTP_201_CREATED, response_model=Dict)
@app.post("/techblog/en/items/", status_code=status.HTTP_201_CREATED, response_model=Dict, tags=["Management"])
async def add_item(item_id: str, name: str, price: float, description: str = None):
"""
Adds a new item to the database. This function demonstrates handling item creation
via multiple POST endpoints, such as a general `/items/` path and a more
descriptive `/add_new_item` path.
- **item_id**: A unique string identifier for the new item.
- **name**: The name of the item.
- **price**: The price of the item.
- **description**: An optional description for the item.
- Returns the newly created item's details.
"""
if item_id in fake_database:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Item with ID '{item_id}' already exists."
)
new_item_data = {"name": name, "price": price, "description": description}
fake_database[item_id] = new_item_data
return {"message": "Item successfully added", "item": {item_id: new_item_data}}
In the get_item_detail function, we've applied three @app.get() decorators: /items/{item_id}, /products/{item_id}, and /catalog/{item_id}. This means that a GET request to any of these paths will trigger the same get_item_detail function. This is incredibly useful for:
- Endpoint Aliases: Providing different names for the same resource. Maybe your marketing team prefers "products" while your internal
apiteam uses "items". - Backward Compatibility: If you're deprecating an old path like
/catalog/{item_id}but want to introduce/items/{item_id}as the new standard, you can keep both active for a transition period, pointing to the same logic. This ensures existing clients don't break immediately. - Flexible Access Patterns: Different parts of your system might naturally refer to the same resource using varied terminology. Mapping these to a single function keeps your business logic centralized.
Implications for OpenAPI Documentation: One of the most impressive aspects of this approach in FastAPI is how seamlessly it integrates with OpenAPI. When you run the application and visit /docs (Swagger UI) or /redoc, you'll find that all three paths (/items/{item_id}, /products/{item_id}, /catalog/{item_id}) are listed as separate endpoints, each with its own entry, but all correctly pointing to the underlying get_item_detail function's parameters and response models. The summary and description provided for the function will apply to all these routes, ensuring consistent and accurate documentation across your api. This consistency is crucial for api consumers, making it easier for them to understand that these different routes fundamentally offer the same functionality.
Advantages of Multiple Decorators: * Simplicity: It's a very straightforward syntax, easy to read and understand. * Code Reusability: The primary benefit β avoids duplicating core logic. * Consistency: Guarantees that all paths deliver the same behavior, error handling, and data transformation, because they share the exact same function body. * Maintainability: Changes to the item retrieval logic only need to be made in one place. * OpenAPI Integration: Automatically generates correct documentation for all mapped routes.
Disadvantages of Multiple Decorators: * Potential for Over-abstraction: If the paths truly have slightly different requirements (e.g., one requires specific authentication that the others don't), force-fitting them into a single function can lead to complex conditional logic within the function, which might be harder to read and test. * Limited Customization per Route: While the summary and description can be set at the function level, if you need wildly different OpenAPI tags, response models, or other metadata per specific route, this approach might become cumbersome. However, FastAPI allows specifying additional parameters (like tags, response_model, status_code) directly in each decorator, offering a degree of customization per route.
Method 2: Reusing Logic with Helper Functions (Internal Abstraction)
While applying multiple decorators directly to a function is powerful, sometimes the "multiple routes" part means that the routes themselves might have slight variations in how they process input or output, but they share a significant portion of their core business logic. In such cases, it's often better to extract the common logic into a separate, non-route-handling helper function. The individual route handlers then call this helper function, potentially adding their own specific pre-processing or post-processing steps.
This method isn't strictly "mapping one function to multiple routes" in the decorator sense, but it achieves the crucial goal of code reuse and maintainability, which is often the underlying motivation for wanting to map functions. It provides a more flexible way to manage logic reuse when routes are similar but not identical.
Let's expand on our item management api:
from fastapi import FastAPI, Depends, HTTPException, status
from typing import Dict, Any, List
app_v2 = FastAPI(
title="Item Management API (V2 - Logic Re-use)",
description="Demonstrates logic reuse with helper functions for different routes.",
version="2.0.0"
)
# In-memory database
fake_database_v2 = {
"report_A": {"title": "Monthly Sales Report", "data": [100, 150, 120], "category": "Sales"},
"report_B": {"title": "Quarterly Financial Overview", "data": [1200, 1300, 1100, 1400], "category": "Finance"},
"report_C": {"title": "Annual Marketing Insights", "data": [5000, 6000], "category": "Marketing"}
}
# --- Shared Business Logic (Helper Function) ---
async def _get_report_data(report_id: str) -> Dict[str, Any]:
"""
Core business logic to retrieve report data from the database.
This function is not a route handler itself.
"""
if report_id not in fake_database_v2:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Report '{report_id}' not found."
)
return fake_database_v2[report_id]
async def _process_report_for_display(report_data: Dict[str, Any], include_metadata: bool = False) -> Dict[str, Any]:
"""
Helper function to process report data for display, potentially adding metadata.
"""
processed_data = {
"report_title": report_data["title"],
"values": report_data["data"],
"total_items": len(report_data["data"])
}
if include_metadata:
processed_data["category"] = report_data["category"]
processed_data["source_system"] = "InternalAnalyticsDB"
return processed_data
# --- Route Handlers Using Helper Functions ---
@app_v2.get("/techblog/en/reports/{report_id}", summary="Get full report details")
async def get_report(report_id: str):
"""
Retrieves complete details for a specific report, including all metadata.
"""
report_data = await _get_report_data(report_id)
return await _process_report_for_display(report_data, include_metadata=True)
@app_v2.get("/techblog/en/reports/{report_id}/summary", summary="Get a summary of a report")
async def get_report_summary(report_id: str):
"""
Retrieves a concise summary of a report, without extensive metadata.
"""
report_data = await _get_report_data(report_id)
return await _process_report_for_display(report_data, include_metadata=False)
@app_v2.get("/techblog/en/analytics/reports/{report_id}", summary="Access analytics report")
async def get_analytics_report(report_id: str):
"""
Provides access to report data through an analytics-specific endpoint.
This demonstrates another alias, utilizing the same core processing logic.
"""
report_data = await _get_report_data(report_id)
# Potentially add analytics-specific post-processing here if needed
processed_report = await _process_report_for_display(report_data, include_metadata=True)
processed_report["analytics_info"] = "Accessed via analytics path"
return processed_report
In this example, _get_report_data handles the database retrieval, and _process_report_for_display handles the common transformation logic. Each route handler (get_report, get_report_summary, get_analytics_report) then calls these helpers. This allows get_report to include full metadata, get_report_summary to offer a trimmed-down version, and get_analytics_report to potentially add analytics-specific fields, all while relying on the same foundational data retrieval and initial processing.
When to prefer helper functions over multiple decorators: * When the routes have significant shared logic but also require unique pre-processing, post-processing, or slightly different response formats. * When the "single function" being mapped would become too complex with conditional statements based on the inferred route. * For better separation of concerns: core business logic resides in non-route functions, making them easier to test independently.
This approach aligns well with general software engineering principles of modularity and single responsibility. It's often a more robust solution for complex apis where logic needs to be shared but not rigidly identical across all endpoints.
Method 3: Advanced Techniques (APIRouter, Custom Decorators)
While APIRouter doesn't directly map one function to multiple routes in the same way stacking decorators does, it's fundamental for organizing large FastAPI applications. You can still apply multiple decorators to a single function within an APIRouter. The APIRouter simply provides a way to prefix routes and group related endpoints, making your api structure cleaner.
from fastapi import APIRouter, Depends, HTTPException, status
from typing import Dict, Any
# Create an APIRouter instance
admin_router = APIRouter(
prefix="/techblog/en/admin",
tags=["Admin Management"],
responses={404: {"description": "Admin operation not found"}},
)
# Re-use the fake database from earlier examples for consistency
fake_database_admin = {
"user_1": {"name": "Admin User Alpha", "role": "admin"},
"user_2": {"name": "Basic User Beta", "role": "user"}
}
async def get_user_from_db(user_id: str) -> Dict[str, Any]:
if user_id not in fake_database_admin:
raise HTTPException(status_code=404, detail="User not found")
return fake_database_admin[user_id]
@admin_router.get("/techblog/en/users/{user_id}", summary="Get User Details (Admin View)")
@admin_router.get("/techblog/en/employees/{user_id}", summary="Get Employee Profile (Admin Access)")
async def get_admin_user_details(user_id: str, user_data: Dict[str, Any] = Depends(get_user_from_db)):
"""
Retrieves detailed information for a user or employee from an administrative perspective.
This function is accessible via multiple paths under the `/admin` prefix,
demonstrating the combination of APIRouter with multi-route mapping.
- **user_id**: The unique identifier for the user/employee.
- Returns comprehensive user/employee data.
"""
return {"message": f"Admin details for {user_id}", "data": user_data}
# Remember to include the router in your main app:
# app.include_router(admin_router)
Here, get_admin_user_details is mapped to /admin/users/{user_id} and /admin/employees/{user_id}. The APIRouter helps organize these administrative endpoints, and the multi-decorator pattern ensures the core logic is shared.
Custom Decorators: For extremely advanced scenarios, you could even create your own custom decorators that dynamically register multiple routes to a function or apply specific pre-processing logic based on the path being accessed. This offers the ultimate flexibility but also adds complexity, and is typically reserved for highly specialized requirements where FastAPI's built-in decorators are insufficient. Such scenarios are rare for most api development.
The interplay between framework-level routing and broader API management solutions is also worth noting. While FastAPI efficiently manages routes within your application, an api gateway like ApiPark operates at a higher level, external to your service. An api gateway can aggregate multiple microservices, apply global policies (authentication, rate limiting, logging), and even perform advanced routing based on request headers, user groups, or other criteria, directing traffic to different versions or instances of your api. This is particularly critical in complex enterprise environments or when dealing with numerous AI models, where OpenAPI definitions from various services need to be unified and controlled. While FastAPI handles the internal mapping, an api gateway handles the external exposure and governance, acting as the single entry point for all api consumers.
Practical Applications and Use Cases
Mapping one function to multiple routes is not merely a syntactic trick; it's a powerful tool that addresses several common challenges in api design and evolution. Let's explore some detailed practical applications.
1. API Versioning (Simple Cases)
API versioning is a critical aspect of managing the lifecycle of your api. As your api evolves, you'll inevitably introduce changes that might break compatibility with existing clients. While robust versioning strategies often involve separate codebases or sophisticated api gateway configurations, for minor or transitional versioning, mapping one function to multiple routes can be a surprisingly effective interim solution.
Scenario: You have an existing endpoint /api/legacy/resource which returns data in an older format. You've developed a new version of the logic that returns a more modern, efficient data structure, and you want to expose it at /api/v2/resource. During a transition period, you want both endpoints to be active, with both serving the new improved logic, ensuring that clients gradually migrate without immediate disruption.
from fastapi import FastAPI
from typing import List, Dict, Any
app_versioning = FastAPI(
title="API Versioning Example",
description="Demonstrates simple API versioning by mapping new and legacy routes to the same function.",
version="1.0.0"
)
# Assume this is the 'new and improved' data retrieval and transformation logic
async def _get_product_list_v2() -> List[Dict[str, Any]]:
"""
Simulates fetching a list of products with updated data structure.
"""
return [
{"id": "prod_101", "name": "Smart Speaker", "price_usd": 99.99, "available": True},
{"id": "prod_102", "name": "Wireless Earbuds", "price_usd": 129.00, "available": False},
{"id": "prod_103", "name": "Robot Vacuum", "price_usd": 499.50, "available": True}
]
@app_versioning.get("/techblog/en/api/v2/products", summary="Get all products (V2)")
@app_versioning.get("/techblog/en/api/latest/products", summary="Get all products (Latest Version)")
@app_versioning.get("/techblog/en/api/products", summary="Get all products (Default, points to V2)")
async def get_all_products_v2():
"""
Returns a list of all products using the latest data format.
This function handles requests for `/api/v2/products`, `/api/latest/products`,
and the unversioned `/api/products`, ensuring all point to the most recent logic.
"""
return await _get_product_list_v2()
@app_versioning.get("/techblog/en/api/legacy/products", summary="Get legacy products (Deprecated)")
async def get_legacy_products():
"""
This endpoint exists for backward compatibility but might internally
transform the V2 data to match a legacy format, or eventually just redirect.
For this example, it also uses the new logic directly to simplify.
"""
# In a real scenario, you might add logic here to transform the _get_product_list_v2()
# output into the old legacy format, or even return a deprecation warning in headers.
print("Warning: Legacy endpoint /api/legacy/products accessed.")
return await _get_product_list_v2()
Here, /api/v2/products, /api/latest/products, and /api/products all trigger get_all_products_v2(). This ensures that new clients, or clients requesting the default /api/products, automatically get the latest logic. If /api/legacy/products still needs to serve the old logic, it would have its own function. But if the goal is to transition clients to the new logic, having the old path also point to the new function (perhaps with a deprecation warning) is a common strategy. This approach reduces code duplication during the sensitive phase of API evolution.
2. Endpoint Aliases and User-Friendly Paths
Sometimes, different teams or departments might have preferred names for the same logical resource. Or, you might want to provide shorter, more memorable aliases for frequently accessed but long-winded paths.
Scenario: Your internal system refers to customer orders as /orders/{order_id}, but the external api for partners might prefer /transactions/{transaction_id}. Both should retrieve the same order details.
from fastapi import FastAPI
from typing import Dict, Any
app_aliases = FastAPI(
title="API Aliases Example",
description="Demonstrates using aliases for API endpoints.",
version="1.0.0"
)
# Simplified order data
order_database = {
"ORD_001": {"amount": 100.00, "status": "completed", "customer_id": "CUST_001"},
"ORD_002": {"amount": 250.50, "status": "pending", "customer_id": "CUST_002"}
}
async def _get_order_details_from_db(order_identifier: str) -> Dict[str, Any]:
"""Helper to fetch order details."""
if order_identifier not in order_database:
raise HTTPException(status_code=404, detail="Order not found")
return order_database[order_identifier]
@app_aliases.get("/techblog/en/orders/{order_id}", summary="Get Order Details")
@app_aliases.get("/techblog/en/transactions/{transaction_id}", summary="Get Transaction Details (Alias for Order)")
@app_aliases.get("/techblog/en/customer_purchases/{purchase_id}", summary="Get Customer Purchase Info (Another Alias)")
async def retrieve_order_information(
order_id: str, # Note: using order_id as the parameter name, FastAPI intelligently maps transaction_id/purchase_id to it
order_details: Dict[str, Any] = Depends(_get_order_details_from_db)
):
"""
Retrieves detailed information for a specific order. This function is accessible
via multiple aliases, catering to different terminologies (orders, transactions, purchases)
while ensuring the same core retrieval logic.
- **order_id**: The identifier for the order/transaction/purchase.
- Returns details of the requested order.
"""
return {"id_used_in_request": order_id, "details": order_details}
Here, /orders/{order_id}, /transactions/{transaction_id}, and /customer_purchases/{purchase_id} all map to retrieve_order_information. FastAPI is intelligent enough to map the path parameter from the URL (transaction_id or purchase_id) to the function's order_id parameter, as long as the parameter names in the path definition match. This keeps your api flexible and user-friendly for diverse consumers.
3. Refactoring and Deprecation
When you restructure your api, you might want to deprecate old endpoints gracefully. Mapping the old and new endpoints to the same function allows for a smooth transition without immediately breaking existing clients.
Scenario: You decide to change your API resource naming from plural /users to singular /user, and to introduce specific actions like /user/{id}/profile. You want /users/{id} to continue working for a while, but eventually be removed.
from fastapi import FastAPI, APIRouter, status, Response, Depends, HTTPException
from typing import Dict, Any
app_refactor = FastAPI(
title="API Refactoring & Deprecation Example",
description="Demonstrates managing API refactoring and deprecation with route mapping.",
version="1.0.0"
)
user_data_store = {
"john_doe": {"name": "John Doe", "email": "john.doe@example.com", "status": "active"},
"jane_smith": {"name": "Jane Smith", "email": "jane.smith@example.com", "status": "inactive"}
}
async def get_user_from_store(user_id: str) -> Dict[str, Any]:
if user_id not in user_data_store:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user_data_store[user_id]
@app_refactor.get(
"/techblog/en/user/{user_id}",
summary="Get User Profile (New)",
description="Retrieves a user's profile using the new, singular resource path."
)
@app_refactor.get(
"/techblog/en/users/{user_id}",
summary="Get User Profile (Deprecated)",
description="**Deprecated:** Please use `/user/{user_id}` instead. This path will be removed in future versions.",
deprecated=True # Mark as deprecated in OpenAPI docs
)
async def get_user_profile(user_id: str, user_info: Dict[str, Any] = Depends(get_user_from_store)):
"""
Fetches a user's profile. This function handles both the new `/user/{user_id}`
and the deprecated `/users/{user_id}` paths, ensuring consistent behavior
during API migration. The deprecated path is marked in OpenAPI documentation.
"""
return {"user_id": user_id, "profile": user_info}
# You can also add a response header for deprecated routes if needed
@app_refactor.get(
"/techblog/en/old_resource/{item_id}",
summary="Access old resource (with warning)",
description="This is an old resource path. Consider migrating to a newer endpoint.",
deprecated=True
)
async def get_old_resource_with_warning(item_id: str, response: Response):
"""
Demonstrates handling a deprecated route by adding a warning header.
"""
response.headers["Warning"] = "299 - 'https://example.com/new_resource' is deprecated, please use the new API."
# Simulate fetching data
return {"message": f"Data for old resource {item_id}", "status": "warning: deprecated"}
By marking the old route (/users/{user_id}) with deprecated=True in the decorator, FastAPI will automatically reflect this status in the OpenAPI documentation (e.g., Swagger UI will show it as "deprecated" or "strike-through"), signaling to developers that they should migrate. The function remains shared, so the underlying logic is always up-to-date.
4. Consolidated Search/Filtering Endpoints
Often, you might have multiple ways clients want to search or filter a collection of resources, but the core filtering and data retrieval logic is largely the same.
Scenario: You have a product listing api. Clients might want to search by a general query string (/products/search?q=keyword) or by a specific product code (/products/code/{code}). Both should leverage the same underlying product lookup mechanism.
from fastapi import FastAPI, Query, HTTPException, status
from typing import List, Dict, Any, Optional
app_search = FastAPI(
title="Consolidated Search Example",
description="Demonstrates a single function handling multiple search/filter routes.",
version="1.0.0"
)
product_inventory = [
{"product_code": "ELC001", "name": "Laptop Pro", "category": "Electronics", "price": 1500},
{"product_code": "ELC002", "name": "Smartphone X", "category": "Electronics", "price": 800},
{"product_code": "BOK001", "name": "Fiction Novel", "category": "Books", "price": 25},
{"product_code": "CLO001", "name": "T-Shirt Basic", "category": "Clothing", "price": 20}
]
async def _find_products_by_criteria(query_str: Optional[str] = None, code_str: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Core logic for finding products based on a general query or specific code.
"""
results = []
if query_str:
search_term = query_str.lower()
for product in product_inventory:
if search_term in product["name"].lower() or search_term in product["category"].lower():
results.append(product)
elif code_str:
for product in product_inventory:
if product["product_code"].lower() == code_str.lower():
results.append(product)
break # Assuming product codes are unique
else:
# If no specific criteria, return all
return product_inventory
if not results:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No products found matching criteria.")
return results
@app_search.get("/techblog/en/products/search", summary="Search Products by Keyword")
@app_search.get("/techblog/en/products/filter", summary="Filter Products (Alias for Search)")
async def search_products(q: Optional[str] = Query(None, description="General keyword to search for in product names or categories")):
"""
Searches for products based on a general keyword query.
This function also serves as an alias for `/products/filter`.
"""
return await _find_products_by_criteria(query_str=q)
@app_search.get("/techblog/en/products/code/{product_code}", summary="Get Product by Code")
async def get_product_by_code(product_code: str):
"""
Retrieves a product by its unique product code.
"""
return await _find_products_by_criteria(code_str=product_code)
@app_search.get("/techblog/en/products/category/{category_name}", summary="Get Products by Category")
async def get_products_by_category(category_name: str):
"""
Retrieves all products within a specified category.
This also uses the underlying search logic effectively.
"""
results = [
product for product in product_inventory
if product["category"].lower() == category_name.lower()
]
if not results:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No products found in category '{category_name}'.")
return results
In this setup, search_products handles both /products/search and /products/filter, using the _find_products_by_criteria helper. The get_product_by_code and get_products_by_category functions also leverage similar patterns. This ensures that the core search logic is consistent and avoids unnecessary duplication, making your api more efficient and easier to maintain.
5. Centralized Security and Authentication Logic
When multiple routes require the exact same authentication and authorization checks, mapping them to a single function (or sharing a dependency) ensures that the security logic is applied consistently and only implemented once.
Scenario: You have several administrative endpoints (/admin/users, /admin/settings, /admin/logs) that all require an authenticated admin user.
from fastapi import FastAPI, Depends, HTTPException, status
from typing import Dict, Any
app_security = FastAPI(
title="Centralized Security Example",
description="Demonstrates applying shared authentication logic to multiple routes.",
version="1.0.0"
)
# A very basic fake token system and user roles
fake_users_db = {
"admin_token": {"username": "admin_user", "roles": ["admin", "editor"]},
"editor_token": {"username": "editor_user", "roles": ["editor"]},
"viewer_token": {"username": "viewer_user", "roles": ["viewer"]}
}
async def get_current_user(token: str) -> Dict[str, Any]:
"""
Dependency to validate a token and return user details.
"""
if token not in fake_users_db:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token."
)
return fake_users_db[token]
async def require_admin_role(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
"""
Dependency to ensure the authenticated user has 'admin' role.
"""
if "admin" not in user.get("roles", []):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Operation requires administrator privileges."
)
return user
@app_security.get("/techblog/en/admin/dashboard", summary="View Admin Dashboard")
@app_security.get("/techblog/en/admin/system_status", summary="Check System Health")
@app_security.get("/techblog/en/admin/audit_logs", summary="Access Audit Logs")
async def get_admin_data(current_admin_user: Dict[str, Any] = Depends(require_admin_role)):
"""
This function handles multiple administrative routes, ensuring that
only users with 'admin' roles can access them. The core logic for
retrieving different admin data would go here (or in helpers).
"""
# In a real application, you'd fetch data specific to the path
# For demonstration, we return a generic message and the user info
path_info = "dashboard/status/logs" # Dynamically get this from request context
return {
"message": f"Welcome to the admin {path_info}! You have admin access.",
"user": current_admin_user["username"],
"roles": current_admin_user["roles"]
}
@app_security.get("/techblog/en/user/profile", summary="Get User Profile")
async def get_user_profile(user: Dict[str, Any] = Depends(get_current_user)):
"""
Allows any authenticated user to view their basic profile.
"""
return {"message": f"Hello, {user['username']}! Here's your profile.", "roles": user["roles"]}
By making require_admin_role a dependency for get_admin_data, any request hitting /admin/dashboard, /admin/system_status, or /admin/audit_logs will first go through the token validation and then the role check. If either fails, an HTTPException is raised, preventing access. This centralized approach simplifies security management and significantly reduces the chance of security vulnerabilities due to missed checks.
This level of internal security management is robust within your api service. However, for a holistic enterprise solution, especially when integrating a multitude of apis, including those interacting with AI models, an api gateway becomes indispensable. ApiPark offers an api gateway that extends these capabilities by providing centralized authentication, authorization, rate limiting, and auditing across all your microservices and AI integrations. It can enforce access policies, manage API keys, and even perform subscription approvals, effectively acting as a unified security layer before requests ever reach your FastAPI service, complementing FastAPI's internal security features with an overarching management framework. This ensures that your OpenAPI definitions are not just well-documented, but also securely governed from the perimeter.
APIPark is a high-performance AI gateway that allows you to securely access the most comprehensive LLM APIs globally on the APIPark platform, including OpenAI, Anthropic, Mistral, Llama2, Google Gemini, and more.Try APIPark now! πππ
Best Practices and Considerations
While mapping one function to multiple routes offers considerable advantages, like any powerful feature, it comes with best practices and considerations to ensure your api remains clear, maintainable, and robust.
1. Clarity and Maintainability: When to Use Which Approach
The choice between multiple decorators, helper functions, or separate route handlers should always prioritize code clarity and long-term maintainability.
- Multiple Decorators: Ideal when the exact same logic, including request parsing, validation, and response generation, is truly shared across slightly different paths for the same HTTP method. Use it for:
- Direct aliases (e.g.,
/itemsand/products). - Simple versioning transitions (e.g.,
/v1/itemsand/v2/itemspointing to the new/v2logic). - Maintaining backward compatibility without duplicating code.
- When the
OpenAPIdocumentation for all routes should be identical in terms of parameters and responses.
- Direct aliases (e.g.,
- Helper Functions (Internal Abstraction): Prefer this when the routes share core business logic but have distinct pre-processing, post-processing, or slightly different input/output requirements. Use it for:
- Routes that retrieve the same data but present it differently (e.g., full details vs. summary).
- Complex data manipulation where sub-steps are shared but orchestration differs.
- When individual routes might need unique dependencies or error handling beyond the shared logic.
- Improving testability of the core logic, which can be tested independently of FastAPI's routing.
- Separate Route Handlers: Stick to this for routes that are conceptually distinct or have significantly different logic. Over-engineering shared functions can lead to conditional spaghetti code (
if path == '/A': do_A() else: do_B()), which is harder to debug and test.
The "don't overdo it" principle is paramount. If mapping multiple routes to one function makes the function's responsibility unclear or forces convoluted conditional logic, it's a strong indicator that separate functions or helper functions would be a better choice.
2. OpenAPI Documentation Impact
FastAPI's OpenAPI generation is a major selling point, and the multi-route approach handles it remarkably well. Each distinct route (e.g., /items/{item_id} and /products/{item_id}) will get its own entry in the Swagger UI/ReDoc documentation, even if they point to the same underlying function.
- Consistent Documentation: The function's
summaryanddescription(from its docstring) will be replicated for all mapped routes. This ensures users understand that these routes perform the same core operation. - Decorator-Level Customization: Remember that you can provide specific
summary,description,tags,response_model,status_code, etc., for each individual decorator. This allows you to tailor theOpenAPIdocumentation for each specific path, even if they share the function. For example, one alias could be marked asdeprecated=True, while others are not.
@app.get("/techblog/en/new-path", summary="New Path for Resource X")
@app.get("/techblog/en/old-path", summary="Old Path (Deprecated) for Resource X", deprecated=True, tags=["Legacy"])
async def get_resource_x():
# ... logic ...
pass
This flexibility allows for clear, precise OpenAPI documentation even with shared functions.
3. Error Handling and Dependencies
- Centralized Error Handling: When a single function serves multiple routes, its error handling logic automatically applies to all of them. This is a huge advantage for consistency. If an item is not found, the
HTTPExceptionraised within the shared function (or its dependencies) will consistently result in a 404 response for all mapped paths. - Seamless Dependencies: FastAPI's dependency injection system works flawlessly with this pattern. Any
Depends()declarations within the shared function will be executed for every request to any of the mapped routes, ensuring consistent validation, authentication, or resource acquisition. This further reinforces the idea of "shared logic."
4. URL Design Principles (RESTfulness)
While mapping one function to multiple routes offers flexibility, it's still crucial to adhere to RESTful api design principles where appropriate. * Resource-Oriented: URLs should primarily identify resources (e.g., /users, /products/{id}). * HTTP Methods for Actions: Use GET for retrieval, POST for creation, PUT/PATCH for updates, DELETE for removal. * Consistency: Strive for consistent naming conventions across your api. Multi-route mapping can help maintain this consistency by allowing different names to point to the same consistent logic.
5. Testing Strategy
Thorough testing is paramount. When you map one function to multiple routes: * Test Each Path: Ensure you write tests that target each individual path (e.g., test /items/{id} and /products/{id} separately), verifying that they all produce the expected output and status codes. * Test Core Logic: If you're using helper functions, ensure those helper functions have comprehensive unit tests independent of the FastAPI routing. This isolates business logic testing. * Dependency Testing: If your shared function relies on dependencies, ensure those dependencies are correctly mocked or implemented in your tests.
6. Performance Considerations
Mapping one function to multiple routes has virtually no performance overhead in FastAPI. The framework internally registers these routes efficiently. The performance bottleneck will always be in your actual business logic (database calls, complex computations), not in the routing mechanism itself. Therefore, you can use this pattern without worrying about introducing latency.
7. Scalability
This pattern fits well within scalable api architectures. * Microservices: If your FastAPI application is a microservice, using this technique within that service keeps its internal api well-organized and DRY (Don't Repeat Yourself). * Load Balancing: When your FastAPI service is behind a load balancer, the load balancer doesn't care about internal route mappings; it simply forwards requests to your service instances. * API Gateway Interaction: As mentioned, an api gateway sits in front of your FastAPI service. It can handle external routing, rate limiting, and security. The internal multi-route mapping within FastAPI streamlines the internal code, making your service simpler and more efficient for the api gateway to manage and expose.
Table: Comparison of Code Reusability Approaches
To further clarify the choices, here's a table comparing the different methods discussed for achieving code reusability in FastAPI, particularly when multiple API entry points might share logic.
| Feature / Aspect | Multiple Decorators (Direct Mapping) | Helper Functions (Internal Abstraction) | Separate Route Handlers (No Reusability) |
|---|---|---|---|
| Core Principle | Multiple paths trigger the exact same route handler function. | Routes call a common non-route function for shared logic. | Each path has its own distinct, self-contained route handler function. |
| Code Duplication | Minimized for the route handler itself. | Minimized for the core business logic. | High potential for duplication if similar logic is needed. |
| Flexibility | Low: Logic is identical across all mapped routes. | Medium: Routes can add pre/post-processing around shared logic. | High: Each route is fully independent. |
OpenAPI Docs |
Each path listed separately, sharing function's doc/params. | Each path listed separately, with its own doc/params. | Each path listed separately, with its own doc/params. |
| Ideal Use Cases | Aliases, simple versioning, strict 1:1 behavior, deprecation. | Similar operations with slight variations, complex business logic reuse. | Unrelated operations, unique logic per endpoint. |
| Maintenance Impact | High: Change in logic affects all paths instantly. | Medium: Changes in helper affect all calling routes. Easier to refactor. | Low: Changes only affect one route. Higher overall code base. |
| Complexity Inside Function | Can increase if conditionals are used for path-specific behavior. | Shared logic remains clean; route handlers handle specific variations. | Low (per function), but high overall if logic is duplicated. |
| Testing | Test each mapped route; implicit testing of shared logic. | Test helper functions (unit tests); test routes (integration tests). | Test each route independently. |
This table serves as a quick reference to guide your decision-making when designing your FastAPI api structure for optimal reusability and maintainability.
Advanced Scenarios and Edge Cases
While the core concept is straightforward, certain advanced scenarios or edge cases warrant deeper consideration when mapping one function to multiple routes.
1. Path Order Matters (for Ambiguous Paths)
FastAPI (and Starlette) processes routes in the order they are defined. For most cases, this isn't an issue. However, if you have overlapping or ambiguous paths, the order of your decorators (or app.include_router() calls) can determine which route takes precedence.
Consider these two routes:
@app.get("/techblog/en/users/me")
async def get_current_user_profile():
return {"message": "Current user's profile"}
@app.get("/techblog/en/users/{user_id}")
async def get_user_by_id(user_id: str):
return {"message": f"Profile for user: {user_id}"}
If /users/me is defined before /users/{user_id}, then a request to /users/me will correctly match the first route. If /users/{user_id} were defined first, then /users/me would likely match /users/{user_id} and user_id would be parsed as "me".
When using multiple decorators on a single function, this "order" is implicitly managed by FastAPI's internal routing table. The important thing is that within the decorators for a single function, the order typically doesn't impact which path matches that function, as long as they are distinct. However, the order of definition of different functions with overlapping paths can matter. This principle applies whether you're mapping one function or using separate ones.
2. Conflicting Path Parameters and Query Parameters
What if two routes mapped to the same function have different path parameters?
Example:
@app.get("/techblog/en/items/by-name/{item_name}")
@app.get("/techblog/en/items/by-id/{item_id}")
async def get_item(item_name: Optional[str] = None, item_id: Optional[str] = None):
if item_name:
return {"message": f"Fetching item by name: {item_name}"}
elif item_id:
return {"message": f"Fetching item by ID: {item_id}"}
return {"message": "No identifier provided"}
This example is problematic. FastAPI requires that if a path parameter is defined in a decorator, it must also be a parameter in the function signature. If you try to run this, FastAPI will complain that item_id is missing from the function signature when it processes /items/by-name/{item_name} and vice versa.
Correct Approach for Varying Identifiers: Instead of trying to squeeze different path parameters into the same function parameter, you should ensure that the function parameters match the path parameters for all decorators, and then handle the logic conditionally. Or, even better, use separate functions with a shared helper.
# More robust approach using a shared helper
async def _fetch_item_logic(identifier: str, by_type: str):
if by_type == "name":
# logic to fetch by name
return {"source": "name", "id": identifier}
elif by_type == "id":
# logic to fetch by id
return {"source": "id", "id": identifier}
raise HTTPException(status_code=400, detail="Invalid identifier type")
@app.get("/techblog/en/items/by-name/{item_name}")
async def get_item_by_name_route(item_name: str):
return await _fetch_item_logic(item_name, "name")
@app.get("/techblog/en/items/by-id/{item_id}")
async def get_item_by_id_route(item_id: str):
return await _fetch_item_logic(item_id, "id")
This clearly shows that while the logic is shared, the route parameters need to be distinct and correctly handled by their respective route functions, which then call a common helper.
3. Granular Security and Permissions
While mapping routes to one function simplifies applying identical security policies, what if one route needs Admin access and another route mapping to the same function needs only Editor access?
This requires careful design with FastAPI's dependency injection. You might need a dependency that checks the actual path of the request (accessible via Request object) and applies conditional permissions.
from fastapi import FastAPI, Depends, HTTPException, status, Request
from typing import Dict, Any
app_granular_security = FastAPI()
# Dummy user roles
fake_users_granular = {
"admin_token": {"username": "admin_user", "roles": ["admin"]},
"editor_token": {"username": "editor_user", "roles": ["editor"]},
}
async def get_current_user_granular(token: str) -> Dict[str, Any]:
if token not in fake_users_granular:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
return fake_users_granular[token]
async def check_permissions_for_route(request: Request, user: Dict[str, Any] = Depends(get_current_user_granular)):
required_role = None
if request.url.path == "/techblog/en/data/critical":
required_role = "admin"
elif request.url.path == "/techblog/en/data/read-only":
required_role = "editor" # Or admin, if editor implies admin implicitly
if required_role and required_role not in user.get("roles", []):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Insufficient permissions. Requires '{required_role}' role.")
return user
@app_granular_security.get("/techblog/en/data/critical", summary="Access Critical Data")
@app_granular_security.get("/techblog/en/data/read-only", summary="Access Read-Only Data")
async def get_sensitive_data(current_user: Dict[str, Any] = Depends(check_permissions_for_route)):
"""
Retrieves sensitive data. Access level depends on the specific path accessed.
`/data/critical` requires 'admin' role, `/data/read-only` requires 'editor' role.
"""
if current_user:
if "/techblog/en/critical" in current_user.get("request_path", ""): # Hypothetical way to pass path or infer in func
# This inner check is redundant if check_permissions_for_route works fully
return {"message": "You accessed critical data.", "user": current_user["username"]}
elif "/techblog/en/read-only" in current_user.get("request_path", ""):
return {"message": "You accessed read-only data.", "user": current_user["username"]}
# In reality, the dependency would raise the HTTPException and this wouldn't be reached on failure.
return {"message": "Data accessed."}
In this scenario, the check_permissions_for_route dependency inspects request.url.path to determine the required role dynamically. This allows a single function to be secured differently based on the specific path invoked. While functional, this approach can make dependencies more complex and should be used judiciously. For very distinct security requirements, separate route handlers with different security dependencies might be clearer.
4. Dynamic Routing (Beyond Decorators)
FastAPI's decorators are excellent for static routes. For highly dynamic routing requirements (e.g., routes generated from a database or configuration file at runtime), you might need to register routes programmatically using app.add_api_route(). This moves away from the multi-decorator pattern but allows for the creation of routes on the fly, potentially all pointing to a generic "dynamic handler" function.
This is a niche requirement for most apis, but it's good to know FastAPI's underlying Starlette framework offers this flexibility for extreme cases. For simpler dynamic needs, APIRouter combined with standard decorators (and perhaps a base path prefix) is usually sufficient.
Understanding these advanced considerations ensures that you not only leverage the power of mapping one function to multiple routes but also apply it intelligently and responsibly, avoiding common pitfalls and maintaining a robust, scalable, and secure api architecture. This nuanced application is what distinguishes a proficient api developer from one who merely knows the syntax.
Conclusion
FastAPI's ability to map a single function to multiple routes is a powerful, yet often underutilized, feature that can significantly streamline your api development process. From enhancing code reusability and simplifying maintenance to providing elegant solutions for API versioning and endpoint aliases, this technique allows developers to craft more efficient, consistent, and easier-to-manage APIs. By centralizing core logic within one function, you drastically reduce the chances of introducing inconsistencies or bugs that often arise from duplicated code, while FastAPI's seamless OpenAPI integration ensures that all your routes are meticulously documented.
We've explored various methods, from simply stacking decorators for identical functionality across different paths to employing helper functions for shared logic with varied processing. Each approach has its merits and ideal use cases, and the intelligent choice between them hinges on the specific needs for flexibility and consistency in your api design.
Furthermore, we've touched upon how FastAPI's internal routing capabilities, while robust, are often complemented by higher-level API management solutions like an api gateway. For applications dealing with a multitude of apis, especially those integrating complex AI models, platforms like ApiPark provide essential services like centralized security, traffic management, and unified OpenAPI governance, ensuring enterprise-grade control and scalability that extends beyond the scope of a single framework. This combination of powerful framework features and comprehensive management platforms forms the backbone of a resilient and high-performing api ecosystem.
Ultimately, mastering the art of mapping one function to multiple routes in FastAPI is about writing cleaner, more elegant, and more sustainable code. It's about thinking strategically about your api's architecture, embracing the principles of DRY, and leveraging the full spectrum of tools that FastAPI offers to build applications that are not only performant but also a joy to develop and maintain. As you continue your journey in api development, remember that thoughtful design, coupled with powerful framework features, is the key to building successful and future-proof services.
Frequently Asked Questions (FAQs)
1. What is the primary benefit of mapping one function to multiple routes in FastAPI?
The primary benefit is enhanced code reusability and maintainability. By allowing different URL paths (and HTTP methods) to trigger the same underlying function, you avoid duplicating business logic. This ensures consistency in behavior across related endpoints, simplifies future modifications, and reduces the overall codebase size, making your api easier to understand, test, and manage.
2. How does FastAPI handle OpenAPI documentation when one function serves multiple routes?
FastAPI intelligently generates OpenAPI documentation for each distinct route separately, even if they point to the same function. This means that if /items/{id} and /products/{id} both map to get_item_detail, both paths will appear in the Swagger UI/ReDoc documentation, each with its own entry. The function's summary, description, parameters, and response models will be consistently displayed for all mapped routes, providing clear and accurate documentation for api consumers. You can also customize summary and description for each decorator.
3. Can I use different HTTP methods for routes mapped to the same function?
Yes, you can. For example, you could have @app.get("/techblog/en/status") and @app.post("/techblog/en/report_status") both point to a function that processes or returns a status, assuming the function logic is generic enough to handle both HTTP methods (e.g., if the POST just updates an internal status and the GET reads it, but the core processing for the "status" object is shared). However, it's generally best practice for a single function to handle one type of operation, usually tied to a single HTTP method. If the logic for GET and POST is very different, separate functions are advisable, potentially sharing a helper function for common sub-logic.
4. What is the difference between mapping one function to multiple routes and using a helper function?
Mapping one function to multiple routes (e.g., using multiple @app.get() decorators on a single function) means the entire route handler function, including its dependencies and direct response, is identical for all mapped paths. Using a helper function, on the other hand, involves creating a separate, non-route-handling function that encapsulates shared business logic. Individual route handlers then call this helper function, allowing them to add unique pre-processing, post-processing, or slightly different responses around the shared core logic. The helper function approach offers more flexibility when routes are similar but not identical, whereas direct mapping is for truly identical behaviors.
5. How does this technique relate to api gateway solutions like APIPark?
FastAPI's multi-route mapping is an internal framework-level feature that optimizes code within a single api service. An api gateway, such as ApiPark, operates at a higher, external layer. It acts as a single entry point for all api consumers, regardless of how many internal services you have. An api gateway can provide centralized api management, including global authentication, authorization, rate limiting, traffic routing (e.g., to different microservices or versions), logging, and OpenAPI governance across your entire api landscape. While FastAPI manages the elegance of routes within your service, an api gateway manages the overarching exposure, security, and governance of all your services, including those utilizing FastAPI's efficient routing. They complement each other to build robust and scalable api ecosystems.
πYou can securely and efficiently call the OpenAI API on APIPark in just two steps:
Step 1: Deploy the APIPark AI gateway in 5 minutes.
APIPark is developed based on Golang, offering strong product performance and low development and maintenance costs. You can deploy APIPark with a single command line.
curl -sSO https://download.apipark.com/install/quick-start.sh; bash quick-start.sh

In my experience, you can see the successful deployment interface within 5 to 10 minutes. Then, you can log in to APIPark using your account.

Step 2: Call the OpenAI API.

