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
{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(...).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./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): |