Path Parameters — Types, Validation and Enum Constraints

Path parameters are the variable parts of a URL — the {post_id} in /posts/{post_id}, the {username} in /users/{username}. FastAPI extracts them from the URL, converts them to the declared Python type, and validates them — all before your handler function runs. A path parameter declared as int will cause FastAPI to return 422 if the URL segment cannot be converted to an integer. Adding Path() metadata applies additional constraints like minimum/maximum values and regex patterns. Python Enum types restrict a path parameter to a fixed, documented set of valid values.

Typed Path Parameters

from fastapi import FastAPI, Path
from enum import Enum

app = FastAPI()

# ── Simple typed path parameter ───────────────────────────────────────────────
@app.get("/posts/{post_id}")
def get_post(post_id: int):       # int → validates and converts from URL string
    return {"post_id": post_id}

# GET /posts/42      → post_id=42 (int)  ✓
# GET /posts/abc     → 422 Validation Error
# GET /posts/3.14    → 422 Validation Error

# ── String path parameter ─────────────────────────────────────────────────────
@app.get("/posts/by-slug/{slug}")
def get_post_by_slug(slug: str):
    return {"slug": slug}

# ── Multiple path parameters ──────────────────────────────────────────────────
@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(user_id: int, post_id: int):
    return {"user_id": user_id, "post_id": post_id}
# GET /users/5/posts/12 → user_id=5, post_id=12
Note: FastAPI maps path parameter names by matching the name in the URL template (curly braces) to the function parameter name. {post_id} in the path maps to the post_id argument — they must match exactly (case-sensitive). The type annotation on the argument tells FastAPI what type to convert to and validate. If you want a path parameter that accepts any string including slashes, use path as the type with Path(): file_path: str = Path(...).
Tip: Use Path() to add constraints and documentation to path parameters — the same way Field() adds constraints to Pydantic model fields. Path(ge=1) ensures the integer is at least 1 (prevents negative IDs), Path(pattern=r"^[a-z0-9-]+$") restricts a slug to valid URL characters, and Path(description="The unique post ID") adds documentation to the Swagger UI. These constraints are enforced automatically and appear in the OpenAPI schema.
Warning: Route definition order matters when a path has both static segments and parameter segments at the same level. /posts/featured must be defined before /posts/{post_id}, or featured will be captured as the value of post_id. FastAPI matches routes in registration order — the first matching route wins. When mixing static paths and path parameters, always define the more specific (static) routes first.

Path() Metadata and Constraints

from fastapi import FastAPI, Path
from typing import Annotated

app = FastAPI()

# ── Constraints with Path() ───────────────────────────────────────────────────
@app.get("/posts/{post_id}")
def get_post(
    post_id: Annotated[int, Path(
        ge          = 1,              # >= 1 (positive IDs only)
        le          = 999_999_999,    # <= 999999999 (reasonable upper bound)
        title       = "Post ID",
        description = "The unique integer identifier for the post",
        example     = 42,
    )]
):
    return {"post_id": post_id}

# ── Slug path parameter with regex ────────────────────────────────────────────
@app.get("/posts/by-slug/{slug}")
def get_post_by_slug(
    slug: Annotated[str, Path(
        pattern     = r"^[a-z0-9][a-z0-9-]*[a-z0-9]$",
        min_length  = 3,
        max_length  = 200,
        description = "URL-friendly slug (lowercase, hyphens only)",
    )]
):
    return {"slug": slug}

# GET /posts/by-slug/hello-world    ✓
# GET /posts/by-slug/UPPERCASE      → 422 (pattern mismatch)
# GET /posts/by-slug/ab             → 422 (too short)

Enum Path Parameters

from enum import Enum
from fastapi import FastAPI

app = FastAPI()

class PostStatus(str, Enum):
    draft     = "draft"
    published = "published"
    archived  = "archived"

class SortOrder(str, Enum):
    asc  = "asc"
    desc = "desc"

@app.get("/posts/by-status/{status}")
def get_posts_by_status(status: PostStatus):
    # status is a validated PostStatus enum member
    return {"status": status.value, "posts": []}

# GET /posts/by-status/published   → status=PostStatus.published  ✓
# GET /posts/by-status/deleted     → 422 Validation Error (not a valid PostStatus value)

# Swagger UI shows a dropdown with "draft", "published", "archived"

@app.get("/posts")
def list_posts(order: SortOrder = SortOrder.desc):
    return {"order": order.value}

Common Mistakes

Mistake 1 — Static route defined after parameterised route

❌ Wrong — /posts/featured never reachable:

@app.get("/posts/{post_id}")   # matches /posts/featured as post_id="featured"
def get_post(post_id: int): ...

@app.get("/posts/featured")    # never reached!
def get_featured(): ...

✅ Correct — static route first:

@app.get("/posts/featured")    # ✓ checked first
def get_featured(): ...

@app.get("/posts/{post_id}")
def get_post(post_id: int): ...

Mistake 2 — Using str path parameter when int is needed

❌ Wrong — no validation on ID values:

@app.get("/posts/{post_id}")
def get_post(post_id: str):    # accepts "abc", "-1", "0" — all invalid IDs
    post = db.query(Post).get(int(post_id))   # ValueError if not a number!

✅ Correct — use int with Path constraints:

@app.get("/posts/{post_id}")
def get_post(post_id: Annotated[int, Path(ge=1)]):   # ✓ validated before handler runs
    ...

Mistake 3 — Forgetting to inherit from str in Enum (breaks JSON serialisation)

❌ Wrong — Enum value not serialisable as string in JSON response:

class Status(Enum):            # not str — value is Status.published, not "published"
    published = "published"

✅ Correct — inherit from str:

class Status(str, Enum):       # ✓ str enum — serialises as "published" in JSON
    published = "published"

Quick Reference

Pattern Code
Integer path param post_id: int
String path param slug: str
With constraints post_id: Annotated[int, Path(ge=1)]
With regex slug: Annotated[str, Path(pattern=r"...")]
Enum path param status: PostStatus where PostStatus(str, Enum)
Multiple params def f(user_id: int, post_id: int):

🧠 Test Yourself

You have @app.get("/posts/{post_id}") with post_id: Annotated[int, Path(ge=1)]. A client requests GET /posts/0. What does FastAPI return?