Search Topics
Search across all FastAPI topics
Path Operations
Core ConceptPath operations are the core building blocks of FastAPI. Each one maps an HTTP method and URL path to a Python function.
You define two endpoints: @app.get("/users") and @app.post("/users"). You curl POST /users with a JSON body. Back comes 405 Method Not Allowed. But you defined POST... right?
$ curl -X POST http://localhost:8000/users -H "Content-Type: application/json" -d '{"name": "Alice"}'
{"detail":"Method Not Allowed"}Question
Why would FastAPI reject a POST request to an endpoint you clearly defined with @app.post()? The answer usually comes down to a typo, a missing import, or running stale code. But here's the deeper lesson: FastAPI is strict about matching the exact HTTP method to the exact path. That strictness is actually a feature.
How FastAPI Routes a Request
Every incoming request has two pieces of identity: the HTTP method (GET, POST, PUT, DELETE) and the URL path. FastAPI checks both. If either one doesn't match a registered operation, you get an error.
Request arrives
POST /users
Match path
/users found?
Match method
POST registered?
Run handler
create_user()
If the path exists but the method doesn't? That's your 405. If neither matches? 404. FastAPI won't guess what you meant.
Routing basics
What you just learned
Every request has two identifiers: HTTP method + URL path
FastAPI matches both — wrong method on a valid path gives 405, not 404
This strictness helps clients understand what went wrong
Your First Path Operation
In FastAPI, a "path operation" is just a Python function with a decorator that says "handle this HTTP method at this URL." That's it. No config files, no routing tables.
from fastapi import FastAPI
app = FastAPI()
# This handles GET requests to "/"
@app.get("/")
async def root():
return {"message": "Hello World"}
# This handles POST requests to "/items"
@app.post("/items")
async def create_item():
return {"item": "created"}Notice: @app.get and @app.post on the same path (/items) are two completely separate operations. The method makes them different.
Explore: HTTP Methods
Each HTTP method has a specific purpose. Explore what each does, whether it's idempotent, and see example requests and responses.
The Full CRUD Toolkit
FastAPI gives you a decorator for every standard HTTP method. Each one maps to a CRUD operation that your API consumers will expect.
@app.get("/items") # Read (list all)
async def list_items():
return []
@app.post("/items") # Create
async def create_item():
return {"created": True}
@app.put("/items/{id}") # Update (full replacement)
async def update_item(id: int):
return {"updated": id}
@app.patch("/items/{id}") # Update (partial)
async def patch_item(id: int):
return {"patched": id}
@app.delete("/items/{id}") # Delete
async def delete_item(id: int):
return {"deleted": id}HTTP methods
What you just learned
GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes
Same path + different methods = different operations
FastAPI auto-rejects methods you haven't explicitly defined
Tags, Summary & Description
Your future self (and your team) will thank you for adding metadata. Tags group related endpoints in Swagger UI, and descriptions explain what each does. It's free documentation.
@app.get(
"/items",
tags=["items"],
summary="List all items",
description="Returns a paginated list of items with optional filtering.",
response_description="A list of Item objects",
)
async def list_items(skip: int = 0, limit: int = 10):
return items[skip : skip + limit]
@app.post(
"/items",
tags=["items"],
summary="Create an item",
status_code=201,
deprecated=False, # Set True to mark as deprecated in docs
)
async def create_item(item: Item):
return itemResponse Status Codes
Ever wonder why a POST returns 200 by default instead of 201? You can fix that. Use the status module for readable constants so you don't have to memorize numbers.
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}
@app.delete("/items/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(id: int):
return None # 204 = no response body
# Common status codes:
# 200 OK → default for GET
# 201 Created → after POST
# 204 No Content → after DELETE
# 404 Not Found → raise HTTPException
# 422 Unprocessable → validation error (automatic)Go Deeper: Combining Path + Query + Body
Insight
A single endpoint can receive data from three places at once: the URL path, the query string, and the request body. FastAPI figures out which is which automatically. Here's the rule it follows:
In the path string?
→ path parameter
Scalar type?
int, str, bool → query
Pydantic model?
→ request body
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import Annotated
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.put("/items/{item_id}")
async def update_item(
item_id: int, # path param (in URL)
q: Annotated[str | None, Query(max_length=50)] = None, # query param (after ?)
item: Item = None, # body (JSON)
):
result = {"item_id": item_id}
if q:
result["query"] = q
if item:
result["item"] = item.model_dump()
return result
# PUT /items/42?q=search
# Body: {"name": "Widget", "price": 9.99}Parameter sources
What you just learned
FastAPI auto-detects where each parameter comes from based on type
Path params come from the URL, scalars become query params, Pydantic models become the body
You can mix all three in a single endpoint function
See It: Parameter Anatomy
Explore how FastAPI extracts path params, query params, and body from a single request.
Think about it...
You have @app.get("/items") and @app.post("/items"). What happens if someone sends a PUT request to /items?
Hint: Think about what the 405 status code specifically means.
Key Points
Decorator Pattern
@app.get(), @app.post() etc. map HTTP methods to functions
Auto Documentation
Every operation auto-appears in /docs with tags and descriptions
Status Codes
Set defaults with status_code — 201 for create, 204 for delete
Parameter Rules
In path → path param, scalar → query, Pydantic model → body
Tags
Group endpoints in Swagger UI with tags=["category"]
Async Support
async def for I/O-bound, def for CPU-bound — both work