Search Topics

Search across all FastAPI topics

GitHub

Path Operations

Core Concept

Path 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?

terminal
$ 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.

main.py
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.

main.py
@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.

main.py
@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 item

Response 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.

main.py
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

main.py
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