Ojasa Mirai

Ojasa Mirai

FastAPI

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🚀 Path Parameter Basics📚 Path Types📚 Validation📚 Required vs Optional📚 Path Converters📚 Order Matters📚 Complex Paths
Fastapi/Path Parameters/Required Optional

Required vs Optional Path Parameters — Advanced Strategies 🏗️

Master the design patterns and strategies for handling required and optional parameters in production APIs.

The Constraint: Path Parameters Are Always Required

In REST architecture, path parameters identify the specific resource. They cannot be truly "optional" in the URL path itself. However, there are strategic patterns to handle optional data:

Pattern 1: Route Ordering for Optional Resources

from fastapi import FastAPI, HTTPException

app = FastAPI()

# Specific resource
@app.get("/files/{file_id}")
async def get_file(file_id: int):
    '''Get a specific file by ID'''
    file = db.get_file(file_id)
    if not file:
        raise HTTPException(status_code=404, detail="File not found")
    return file

# List all resources (handles the "no specific ID" case)
@app.get("/files")
async def list_files(
    skip: int = 0,
    limit: int = 10,
    search: Optional[str] = None
):
    '''List files with optional query parameters'''
    return db.list_files(skip=skip, limit=limit, search=search)

# Special cases (must come BEFORE generic routes)
@app.get("/files/special/recent")
async def get_recent_files():
    '''Get recently updated files'''
    return db.get_recent_files()

Key Point: More specific routes must be defined BEFORE generic ones.

Pattern 2: Versioning with Optional Sections

# v1 - Simple, required ID only
@app.get("/api/v1/items/{item_id}")
async def get_item_v1(item_id: int):
    return {"item_id": item_id}

# v2 - Richer, with optional expansions
@app.get("/api/v2/items/{item_id}")
async def get_item_v2(
    item_id: int,
    expand: Optional[str] = Query(None, regex="^(user|reviews|related)$")
):
    '''
    Optional expand parameter to include related data.
    /api/v2/items/123?expand=user ← Include user details
    /api/v2/items/123?expand=reviews ← Include reviews
    /api/v2/items/123 ← Basic info only
    '''
    item = db.get_item(item_id)

    if expand == "user":
        item["user"] = db.get_user(item["user_id"])
    elif expand == "reviews":
        item["reviews"] = db.get_reviews(item_id)

    return item

Pattern 3: Composite Routes for Complex Scenarios

from typing import Optional

# Single resource
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return db.get_user(user_id)

# User with optional data
@app.get("/users/{user_id}/full")
async def get_user_full(user_id: int):
    '''Get user with all related data'''
    user = db.get_user(user_id)
    user["posts"] = db.get_user_posts(user_id)
    user["comments"] = db.get_user_comments(user_id)
    user["followers"] = db.get_user_followers(user_id)
    return user

# User timeline (alternative representation)
@app.get("/users/{user_id}/timeline")
async def get_user_timeline(user_id: int, limit: int = 20):
    '''Get user's activity timeline'''
    return db.get_timeline(user_id, limit=limit)

Pattern 4: Matryoshka Routes (Nested Hierarchy)

# Level 1: Organization
@app.get("/orgs/{org_id}")
async def get_organization(org_id: int):
    return db.get_org(org_id)

# Level 2: Team within organization
@app.get("/orgs/{org_id}/teams/{team_id}")
async def get_team(org_id: int, team_id: int):
    team = db.get_team(team_id)
    if team["org_id"] != org_id:
        raise HTTPException(status_code=404)
    return team

# Level 3: Member within team
@app.get("/orgs/{org_id}/teams/{team_id}/members/{member_id}")
async def get_member(org_id: int, team_id: int, member_id: int):
    member = db.get_member(member_id)
    if member["team_id"] != team_id or member["org_id"] != org_id:
        raise HTTPException(status_code=404)
    return member

# Also support shortcuts
@app.get("/teams/{team_id}/members/{member_id}")
async def get_member_direct(team_id: int, member_id: int):
    '''Direct access without org_id if known'''
    member = db.get_member(member_id)
    if member["team_id"] != team_id:
        raise HTTPException(status_code=404)
    return member

Pattern 5: Error Handling for Missing Required Parameters

from fastapi import HTTPException, status

@app.get("/resources/{resource_id}")
async def get_resource(resource_id: int):
    '''Handle missing required parameters appropriately'''
    # This code only runs if resource_id is a valid integer
    resource = db.get_resource(resource_id)

    if not resource:
        # Resource ID was valid, but doesn't exist
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Resource {resource_id} not found"
        )

    return resource

# FastAPI automatically returns 422 if resource_id is invalid:
# /resources/abc → 422 Unprocessable Entity
# /resources/123 but doesn't exist → 404 Not Found

Best Practices for Required vs Optional

from fastapi import FastAPI, Query, Path
from typing import Optional
from pydantic import validator

app = FastAPI()

@app.get("/articles/{article_id}")
async def get_article(
    # Required path parameter - validates type
    article_id: int = Path(..., gt=0, description="Article ID (required)"),

    # Optional query parameters - for filtering/options
    include_comments: bool = Query(False),
    include_author_details: bool = Query(False),
    lang: Optional[str] = Query(None, regex="^[a-z]{2}$")
):
    '''
    Example of proper required vs optional handling.

    Required: article_id (in path)
    Optional: include_comments, include_author_details, lang (in query)
    '''
    article = db.get_article(article_id)

    if include_comments:
        article["comments"] = db.get_comments(article_id)

    if include_author_details:
        article["author"] = db.get_user(article["author_id"])

    if lang:
        article["translated_content"] = db.get_translation(article_id, lang)

    return article

Design Decision Matrix

ScenarioSolutionExample
Always need identifierRequired path param`/users/123`
Sometimes need extra dataOptional query param`/users/123?include=posts`
Different operationsMultiple routes`/users/{id}` vs `/users/search`
Alternative access methodShortcut route`/teams/{id}/members/{mid}` and `/members/{mid}`
Version differencesAPI versioning`/v1/...` vs `/v2/...`

Performance Considerations

# Cache optional expansions
from functools import lru_cache

@lru_cache(maxsize=128)
def get_user_with_cache(user_id: int):
    return db.get_user(user_id)

@app.get("/users/{user_id}")
async def get_user(user_id: int, include_posts: bool = False):
    user = get_user_with_cache(user_id)

    if include_posts:
        # Expensive operation only when requested
        user["posts"] = db.get_user_posts(user_id)

    return user

🔑 Key Takeaways

  • ✅ Path parameters are always required in REST
  • ✅ Use query parameters for optional data
  • ✅ Use route ordering for different use cases
  • ✅ Define specific routes BEFORE generic ones
  • ✅ Validate required parameters with type hints
  • ✅ Return appropriate error codes (404 vs 422)
  • ✅ Use patterns like versioning or expansion for optional features
  • ✅ Cache expensive optional operations
  • ✅ Design APIs with clear, predictable patterns

Challenge: Design an API with at least 3 levels of required parameters and optional query filters.


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn