Skip to content

    Chapter 9, Basic

    Combining path, query, and body

    In a real API, the same route receives an identifier, an option, and a set of data. FastAPI separates the three by how you declare each parameter.

    How FastAPI decides where each parameter comes from

    The rule is simple and applies to the entire route:

    If the parameter...It comes from...
    Appears in the path ({id})The path
    Is a simple type (int, str, bool) outside the pathThe parameters (?x=1)
    Is a Pydantic modelThe body (JSON)
    basico/cap09_combinando_parametros.pylinhas 10 a 36
    from fastapi import FastAPI, HTTPException
    from fastapi.testclient import TestClient
    from pydantic import BaseModel
    
    app = FastAPI()
    
    
    class UsuarioEntrada(BaseModel):
        nome: str
        idade: int
    
    
    usuarios: dict[int, UsuarioEntrada] = {1001: UsuarioEntrada(nome="Mohit", idade=23)}
    
    
    @app.put("/usuarios/{usuario_id}")
    def atualizar(usuario_id: int, usuario: UsuarioEntrada, notificar: bool = False):
        if usuario_id not in usuarios:
            raise HTTPException(status_code=404, detail="Usuário não encontrado")
        usuarios[usuario_id] = usuario
        return {"mensagem": "Usuário atualizado", "notificar": notificar, "dados": usuario}
    
    
    cliente = TestClient(app)
    resposta = cliente.put("/usuarios/1001?notificar=true", json={"nome": "Mohit", "idade": 24})
    print(resposta.status_code, resposta.json())
    print(cliente.put("/usuarios/9", json={"nome": "X", "idade": 1}).json())
    
    Output
    200 {'mensagem': 'Usuário atualizado', 'notificar': True, 'dados': {'nome': 'Mohit', 'idade': 24}}
    {'detail': 'Usuário não encontrado'}
    

    The three arrived at the same time: usuario_id from the path, notificar from the query and usuario of the body. You can check how FastAPI classified each one in the OpenAPI document:

    basico/cap09_combinando_parametros.pylinhas 38 a 40
    operacao = app.openapi()["paths"]["/usuarios/{usuario_id}"]["put"]
    print([(p["name"], p["in"]) for p in operacao["parameters"]])
    print("requestBody" in operacao)
    
    Output
    [('usuario_id', 'path'), ('notificar', 'query')]
    True
    

    Each part has a role: the path identifies which resource, the parameters adjusts how the operation happens, and the body carries what will be recorded.

    Multiple models in the body

    If the route declares two models, FastAPI expects a JSON with a key for each one, instead of a "loose" body:

    basico/cap09_combinando_parametros.pylinhas 45 a 59
    class Item(BaseModel):
        nome: str
    
    
    class Comprador(BaseModel):
        email: str
    
    
    @app.post("/pedidos")
    def criar_pedido(item: Item, comprador: Comprador):
        return {"item": item.nome, "comprador": comprador.email}
    
    
    corpo = {"item": {"nome": "Caneta"}, "comprador": {"email": "a@b.com"}}
    print(cliente.post("/pedidos", json=corpo).json())
    
    Output
    {'item': 'Caneta', 'comprador': 'a@b.com'}
    

    A simple value inside the body

    A simple type goes to the query, unless you mark it with Body. This is useful to add a loose field to the body, alongside a model:

    basico/cap09_combinando_parametros.pylinhas 64 a 74
    from typing import Annotated
    
    from fastapi import Body
    
    
    @app.post("/avaliacoes")
    def avaliar(item: Item, nota: Annotated[int, Body(ge=1, le=5)]):
        return {"item": item.nome, "nota": nota}
    
    
    print(cliente.post("/avaliacoes", json={"item": {"nome": "Caneta"}, "nota": 5}).json())
    
    Output
    {'item': 'Caneta', 'nota': 5}
    

    A body with a single key

    With a single model, the body is the model itself. If you want it to come wrapped in a key (to maintain the format when the body grows later), use embed=True:

    basico/cap09_combinando_parametros.pylinhas 79 a 84
    @app.post("/itens")
    def criar_item(item: Annotated[Item, Body(embed=True)]):
        return item
    
    
    print(cliente.post("/itens", json={"item": {"nome": "Régua"}}).json())
    
    Output
    {'nome': 'Régua'}
    

    Who identifies, who filters, who carries

    Before deciding where a data goes, I ask myself what its role is. If it identifies the resource, it's a path. If it adjusts an operation or filters a list, it's a query. If it's the content that will be created or altered, it's the body.

    Exercise 1

    Update a product with discount

    Create PUT /produtos/{produto_id} with body ProdutoEntrada (nome, preco) and the query parameter aplicar_desconto (default False). If true, save the price with a 10% discount. Return 404 for a non-existent id.