Chapter 4, Basic
Path parameters
A route that changes according to a piece of the URL: product 12, user 7. A path parameter avoids creating a route for each value.
A dynamic piece of the URL
Between braces, the snippet becomes a variable that arrives at the function with the same name. Without a type, it arrives as text. With a type, FastAPI converts and validates:
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/usuarios/{usuario_id}")
def obter_usuario(usuario_id: int):
return {"usuario_id": usuario_id, "tipo": type(usuario_id).__name__}
cliente = TestClient(app)
print(cliente.get("/usuarios/200").json())
{'usuario_id': 200, 'tipo': 'int'}
The "200" from the URL arrived at the function as the integer 200. When the value is not of the requested type, the request is rejected before the function runs, with a 422 that states what went wrong:
resposta = cliente.get("/usuarios/abc")
erro = resposta.json()["detail"][0]
print(resposta.status_code)
print(erro["loc"], erro["type"])
print(erro["msg"])
422
['path', 'usuario_id'] int_parsing
Input should be a valid integer, unable to parse string as an integer
The field loc indicates where the problem is (the path, the parameter usuario_id), the type classifies the error, and the msg explains. No line of your code was written for this.
Restrictions with `Path`
To impose rules beyond the type (greater than zero, at most so many), use Path along with Annotated. The restrictions also appear in the documentation:
from typing import Annotated
from fastapi import Path
@app.get("/produtos/{produto_id}")
def obter_produto(
produto_id: Annotated[int, Path(gt=0, description="Identificador do produto")],
):
return {"produto_id": produto_id}
print(cliente.get("/produtos/5").status_code, cliente.get("/produtos/0").status_code)
print(cliente.get("/produtos/0").json()["detail"][0]["msg"])
200 422
Input should be greater than 0
The order of routes matters
FastAPI evaluates the routes in the order they were declared, and the first one that matches wins. A fixed route like /usuarios/eu needs to come before from the dynamic /usuarios/{usuario_id}, otherwise the text eu is taken as the value of the parameter:
app_errado = FastAPI()
@app_errado.get("/pessoas/{pessoa_id}")
def pessoa(pessoa_id: int):
return {"id": pessoa_id}
@app_errado.get("/pessoas/eu")
def eu_errado():
return {"quem": "eu"}
print("dinâmica primeiro:", TestClient(app_errado).get("/pessoas/eu").status_code)
app_certo = FastAPI()
@app_certo.get("/pessoas/eu")
def eu_certo():
return {"quem": "eu"}
@app_certo.get("/pessoas/{pessoa_id}")
def pessoa_certa(pessoa_id: int):
return {"id": pessoa_id}
print("fixa primeiro:", TestClient(app_certo).get("/pessoas/eu").json())
dinâmica primeiro: 422
fixa primeiro: {'quem': 'eu'}
Values from a closed list
When only some values are accepted, use a Enum. The validation and documentation will list the options:
from enum import Enum
class Categoria(str, Enum):
livros = "livros"
jogos = "jogos"
@app.get("/catalogo/{categoria}")
def catalogo(categoria: Categoria):
return {"categoria": categoria.value}
print(cliente.get("/catalogo/jogos").json())
resposta = cliente.get("/catalogo/roupas")
print(resposta.status_code, resposta.json()["detail"][0]["type"])
{'categoria': 'jogos'}
422 enum
Paths with slashes
A normal path parameter does not accept /. When the value is a file path, the converter :path captures the entire rest:
@app.get("/arquivos/{caminho:path}")
def arquivo(caminho: str):
return {"caminho": caminho}
print(cliente.get("/arquivos/docs/2026/relatorio.pdf").json())
{'caminho': 'docs/2026/relatorio.pdf'}
Path parameter identifies a resource
I use the path to identify something (user 7, request 123), and the query string (next chapter) to filter or adjust (
?ativo=true). If the URL without that value wouldn't make sense, the value belongs to the path.
Exercise 1
A product, validated
Create GET /itens/{item_id} with item_id integer greater than zero. Check that 5 returns 200, that 0 and abc return 422.