Curso de Python / Criar uma API JSON com Flask e validar entradas

Uma pequena API Flask conecta requisições HTTP a funções Python e devolve dados JSON. Confira o objeto recebido, campos obrigatórios, tipos e limites, usando códigos de status para distinguir uma criação válida de uma entrada rejeitada.

Definir o escopo local e preparar o ambiente

Esta API é uma prática acessível pelo próprio computador. As despesas ficam em uma lista na memória do processo: desaparecem quando ele termina e não são compartilhadas automaticamente com outros processos. Persistência, autenticação, autorização, implantação e concorrência exigem projeto adicional para um serviço público.

Selecione o ambiente virtual da prática e instale Flask, que é um pacote externo:

python -m pip install flask

Salve os arquivos seguintes em uma pasta própria, em UTF-8. Evite chamar o programa de flask.py, pois esse nome conflita com o pacote. A introdução oficial ao Flask apresenta a aplicação e seu servidor de desenvolvimento.

JSON é um formato de representação, não um mecanismo de armazenamento durável. As aulas de dados JSON e armazenamento com SQLite mostram a diferença entre representar um objeto e manter seus dados depois da execução.

Salvar e executar a aplicação completa

Salve o programa como app.py:

from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []

@app.errorhandler(413)
def too_large(error):
    return jsonify(error="request body too large"), 413

@app.get("/api/expenses")
def list_expenses():
    return jsonify(expenses)

@app.post("/api/expenses")
def add_expense():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return jsonify(error="expected JSON object"), 400
    name, amount = data.get("name"), data.get("amount")
    if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
        return jsonify(error="invalid name or amount"), 400
    row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
    expenses.append(row)
    return jsonify(row), 201

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

Para experimentar o servidor, execute python app.py no terminal do ambiente selecionado. No macOS ou Ubuntu, fora de um ambiente virtual, use python3 se esse for o comando do interpretador. Abra http://127.0.0.1:5000/api/expenses no navegador para fazer um GET: inicialmente deve aparecer uma lista vazia. A raiz / não tem rota definida neste exemplo.

O endereço 127.0.0.1 limita a escuta à interface de loopback. Se a porta 5000 já estiver ocupada, altere o número no código e no endereço usado para acessar a API. O servidor de desenvolvimento não deve ser exposto como um serviço de produção; o exemplo mantém debug=False. Ao terminar a experiência com o servidor, pressione Ctrl+C no terminal.

@app.get associa a leitura da lista a uma função; @app.post associa a criação de uma despesa a outra. Uma criação válida devolve o novo objeto com status 201; uma leitura válida devolve a lista com status 200. Entradas rejeitadas pelas condições explícitas devolvem um objeto de erro e status 400.

Verificar as rotas com o cliente de testes

Salve o segundo arquivo completo como test_client.py, ao lado de app.py. O cliente de testes chama o fluxo de requisições da aplicação diretamente; você não precisa manter um servidor separado aberto para executar esse arquivo.

from app import app, expenses
expenses.clear()
with app.test_client() as client:
    created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
    assert created.status_code == 201
    for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
        response = client.post("/api/expenses", json=payload)
        assert response.status_code == 400 and response.is_json
    assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
    too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
    assert too_large.status_code == 413 and too_large.is_json
    rows = client.get("/api/expenses")
    assert rows.status_code == 200 and len(rows.get_json()) == 1
    print(created.status_code, rows.get_json()[0]["amount"])
print("validation and 413 checks passed")

Execute sem a opção de otimização -O, que pode remover as instruções assert:

python test_client.py

A saída esperada tem duas linhas: 201 8000 e validation and 413 checks passed. A primeira confirma a criação e a quantia consultada; a segunda indica que o script passou por suas verificações sem interromper por uma asserção.

O teste limpa somente a lista importada no próprio processo, cria um registro válido e verifica rejeição de booleano, negativo, nome em branco, array e JSON quebrado. Depois verifica uma requisição maior que o limite e confirma que somente o registro válido permaneceu. A lista de um servidor iniciado em outro processo é independente dessa lista do teste.

Esse procedimento não verifica a rede real, um proxy reverso ou uma implantação pública. Uma resposta correta no cliente de testes e uma resposta acessível pelo servidor local são evidências de camadas diferentes. Mantenha clara essa distinção ao ampliar a suíte.

Interpretar a validação e a duração dos dados

request.get_json(silent=True) devolve um resultado ausente quando não consegue obter o JSON esperado; a aplicação o rejeita porque não é um dicionário. Mesmo um JSON válido pode ter o formato errado, como uma lista em vez de um objeto.

O nome precisa ser uma string não vazia após strip(); o valor salvo é aparado. A quantia precisa ter exatamente o tipo int e ser não negativa. JSON true vira um booleano Python; como isinstance(True, int) é verdadeiro, o teste de tipo exato impede que esse booleano seja aceito como dinheiro.

MAX_CONTENT_LENGTH=4096 limita o corpo da requisição a 4096 bytes. O exemplo registra um tratador de 413 para devolver um erro JSON com o status correto. Isso não limita sozinho a quantidade de registros já acumulados, nem estabelece todas as restrições de recursos ou de domínio. Consulte a configuração de MAX_CONTENT_LENGTH e o tratamento de erros do Flask.

O contrato JSON cobre estas rotas e os erros tratados. Outros erros padrão, como uma rota inexistente, podem devolver HTML; uma requisição recusada antes pelo servidor ou proxy também não está coberta por esse tratador. Não prometa que todo erro de qualquer camada terá o mesmo corpo JSON.

  1. Execute o teste original e confirme as duas linhas de sucesso. Confira que os casos inválidos não aumentaram a lista.
  2. Experimente um nome com espaços nas extremidades e uma quantia zero: os valores válidos devem ser aceitos, com nome aparado.
  3. Inicie o servidor local, consulte a lista, encerre-o e reinicie-o. As despesas anteriores daquele processo não devem persistir. Não use essa lista como armazenamento definitivo.

Os IDs são calculados a partir do tamanho de uma lista em um processo. Essa estratégia é suficiente para a demonstração controlada, mas não substitui uma política de identificadores e sincronização para um serviço concorrente. Ao adicionar armazenamento durável, defina as transações e a política de repetição de requisições antes de considerar a criação concluída.