Skip to content

    Chapter 11, Basic

    Status codes and errors with HTTPException

    The status code is the first thing the client reads. Getting the code right is what makes a client (and a human) know, without reading the body, if it worked, if the error was theirs or the server's.

    The codes that matter

    CodeNameWhen to use
    200OKThe read or modification was successful
    201CreatedA new resource has been created
    204No ContentIt worked, and there is no body (a DELETE, for example)
    400Bad RequestThe request is malformed or violates a business rule
    401UnauthorizedThe client is not authenticated (who are you?)
    403ForbiddenThe client is authenticated, but does not have permission
    404Not FoundThe resource does not exist
    409ConflictConflict with the current state (an email is already registered)
    422Unprocessable EntityThe submitted data does not pass validation (FastAPI uses it alone)
    500Internal Server ErrorA bug on the server

    The first ten digits tell the story: 2xx is success, 4xx is client error (the caller can fix it) and 5xx is server error (the client cannot do anything).

    Declare the success status

    By default, every route returns 200. For a POST that creates, the correct response is 201, and the module status provides named constants that read themselves:

    basico/cap11_status_http.pylinhas 10 a 37
    from fastapi import FastAPI, HTTPException, status
    from fastapi.testclient import TestClient
    from pydantic import BaseModel
    
    app = FastAPI()
    
    
    class UsuarioEntrada(BaseModel):
        nome: str
        email: str
    
    
    usuarios: dict[str, UsuarioEntrada] = {}
    
    
    @app.post("/usuarios", status_code=status.HTTP_201_CREATED)
    def criar(usuario: UsuarioEntrada):
        if usuario.email in usuarios:
            raise HTTPException(status.HTTP_409_CONFLICT, detail="E-mail já cadastrado")
        usuarios[usuario.email] = usuario
        return {"mensagem": "Usuário criado", "email": usuario.email}
    
    
    cliente = TestClient(app)
    corpo = {"nome": "Mohit", "email": "mohit@exemplo.com"}
    print(cliente.post("/usuarios", json=corpo).status_code)
    repetido = cliente.post("/usuarios", json=corpo)
    print(repetido.status_code, repetido.json())
    
    Output
    201
    409 {'detail': 'E-mail já cadastrado'}
    

    HTTPException: interrupt and respond with the error

    The raise HTTPException(...) interrupts the function immediately and returns the error response, with the detail that you chose. A 404 classic:

    basico/cap11_status_http.pylinhas 42 a 51
    @app.get("/usuarios/{email}")
    def obter(email: str):
        if email not in usuarios:
            raise HTTPException(status_code=404, detail="Usuário não encontrado")
        return usuarios[email]
    
    
    print(cliente.get("/usuarios/mohit@exemplo.com").json())
    resposta = cliente.get("/usuarios/nao@existe.com")
    print(resposta.status_code, resposta.json())
    
    Output
    {'nome': 'Mohit', 'email': 'mohit@exemplo.com'}
    404 {'detail': 'Usuário não encontrado'}
    

    The HTTPException also accepts headers, which the 401 uses to tell the client how to authenticate (chapter 20).

    204: success without body

    A DELETE successful has nothing to say. The status 204 means that, and the response comes without a body:

    basico/cap11_status_http.pylinhas 56 a 64
    @app.delete("/usuarios/{email}", status_code=status.HTTP_204_NO_CONTENT)
    def remover(email: str):
        if email not in usuarios:
            raise HTTPException(status.HTTP_404_NOT_FOUND, "Usuário não encontrado")
        del usuarios[email]
    
    
    resposta = cliente.delete("/usuarios/mohit@exemplo.com")
    print(resposta.status_code, resposta.content)
    
    Output
    204 b''
    

    Document the errors that the route can return

    By default, the documentation only knows success and the 422. The parameter responses adds the other codes to the OpenAPI document, so that those consuming the API know what to expect:

    basico/cap11_status_http.pylinhas 69 a 75
    @app.get("/contas/{conta_id}", responses={404: {"description": "Conta não encontrada"}})
    def conta(conta_id: int):
        raise HTTPException(status_code=404, detail="Conta não encontrada")
    
    
    respostas = app.openapi()["paths"]["/contas/{conta_id}"]["get"]["responses"]
    print(sorted(respostas))
    
    Output
    ['200', '404', '422']
    

    Never return an error with status 200

    Much material teaches how to write return {"error": "não encontrado"}. The result is a response of success (200) with an error message inside, and the client, who looks at the status first, thinks everything went well:

    basico/cap11_status_http.pylinhas 80 a 86
    @app.get("/errado/{item_id}")
    def errado(item_id: int):
        return {"error": "Item não encontrado"}
    
    
    resposta = cliente.get("/errado/1")
    print(resposta.status_code, resposta.json())
    
    Output
    200 {'error': 'Item não encontrado'}
    

    And that "envelope" with status and message?

    Some guides recommend always returning {"status": "success", "message": ..., "data": ...}. I don't do that: the HTTP protocol already has the status, and an envelope duplicates the information (and invites the error from the previous paragraph). What matters is a consistent error format throughout the API, and the next chapter shows how to ensure this in one place.

    Exercise 1

    A withdrawal with insufficient balance

    Create POST /saques that receives {"valor": ...} and has a balance of 100. If the amount exceeds the balance, return 400 with the message "Saldo insuficiente". If valid, deduct and return 201 with the new balance.