Nesta aula, vamos colocar em prática os conceitos de desenvolvimento web com Python criando uma API simples, mas funcional. Uma API (Application Programming Interface) é um conjunto de regras que permite que diferentes sistemas se comuniquem. No contexto web, uma API geralmente expõe endpoints (URLs) que retornam dados em formatos como JSON, permitindo que clientes (como aplicativos front-end ou outros servidores) consumam esses dados.

Utilizaremos o Flask, um micro-framework Python que permite criar aplicações web de forma rápida e elegante. Flask é leve, fácil de aprender e amplamente utilizado em projetos pequenos e médios. Ao final desta aula, você terá uma API que gerencia uma lista de itens, com operações básicas de leitura e escrita, além de tratamento de erros personalizado.

Framework leve

O Flask é chamado de micro-framework porque mantém o núcleo simples e extensível. Ele não inclui muitas funcionalidades por padrão, como autenticação ou ORM, mas permite que você adicione apenas o que precisa através de extensões. Isso o torna ideal para APIs pequenas e para aprender os fundamentos do desenvolvimento web.

Para começar, você precisa instalar o Flask. Isso pode ser feito via pip, o gerenciador de pacotes do Python. Recomenda-se criar um ambiente virtual para isolar as dependências do projeto. No terminal, execute:

python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate  # Windows
pip install flask

Depois de instalado, crie um arquivo chamado app.py e importe o Flask. O código mínimo para uma aplicação é:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return 'Olá, mundo!'

if __name__ == '__main__':
    app.run(debug=True)

Quando você executa python app.py, o servidor de desenvolvimento é iniciado em http://localhost:5000. A função home é associada à rota raiz / e retorna uma string simples. O parâmetro debug=True ativa o modo de depuração, que recarrega o servidor automaticamente a cada alteração no código e exibe mensagens de erro detalhadas.

Rotas

Rotas são os caminhos que definem como a aplicação responde a diferentes URLs. No Flask, usamos o decorador @app.route() para associar uma função a uma URL específica. Além de rotas estáticas, podemos definir rotas dinâmicas, capturando partes da URL como variáveis.

Por exemplo, para uma API de itens, podemos ter as seguintes rotas:

from flask import Flask, request, jsonify

app = Flask(__name__)

# Dados em memória (simulando um banco de dados)
itens = []

@app.route('/itens', methods=['GET'])
def listar_itens():
    return jsonify(itens)

@app.route('/itens/<int:id>', methods=['GET'])
def obter_item(id):
    # Implementação posterior
    pass

@app.route('/itens', methods=['POST'])
def criar_item():
    # Implementação posterior
    pass

@app.route('/itens/<int:id>', methods=['PUT'])
def atualizar_item(id):
    # Implementação posterior
    pass

@app.route('/itens/<int:id>', methods=['DELETE'])
def deletar_item(id):
    # Implementação posterior
    pass

Ao especificar <int:id>, estamos dizendo que o parâmetro id deve ser um inteiro. O Flask converte automaticamente o valor da URL para o tipo indicado. Isso facilita a validação básica e evita erros de tipo. Além disso, o parâmetro methods permite restringir os métodos HTTP aceitos (GET, POST, PUT, DELETE, etc.).

Para testar as rotas, você pode usar ferramentas como Postman, Insomnia ou até mesmo o curl no terminal. Por exemplo, para listar os itens, você faria uma requisição GET para http://localhost:5000/itens. Inicialmente, a lista estará vazia, mas logo implementaremos a criação de itens.

JSON

JSON (JavaScript Object Notation) é o formato padrão para troca de dados em APIs web. O Flask fornece o método jsonify que converte um dicionário ou lista Python em uma resposta JSON com o cabeçalho Content-Type: application/json adequado.

Vamos implementar a criação e listagem de itens. Cada item será um dicionário com id, nome e descricao. Para gerar IDs únicos, podemos usar um contador simples:

from flask import Flask, request, jsonify

app = Flask(__name__)

itens = []
proximo_id = 1

@app.route('/itens', methods=['GET'])
def listar_itens():
    return jsonify(itens)

@app.route('/itens', methods=['POST'])
def criar_item():
    global proximo_id
    dados = request.get_json()
    if not dados or not 'nome' in dados:
        return jsonify({'erro': 'O campo nome é obrigatório'}), 400
    item = {
        'id': proximo_id,
        'nome': dados.get('nome'),
        'descricao': dados.get('descricao', '')
    }
    itens.append(item)
    proximo_id += 1
    return jsonify(item), 201

A função request.get_json() obtém o corpo da requisição e tenta interpretá-lo como JSON. Se o conteúdo não for JSON válido, o Flask retornará um erro 400 automaticamente. Adicionamos uma validação manual para garantir que o campo nome esteja presente. Se não estiver, retornamos uma mensagem de erro com status 400 (Bad Request).

Quando criamos um item, retornamos o item criado com status 201 (Created), que é o código padrão para criação bem-sucedida. O método jsonify também define o cabeçalho Content-Type e serializa o dicionário para JSON.

Para testar, envie uma requisição POST para http://localhost:5000/itens com o corpo JSON:

{
  "nome": "Teclado",
  "descricao": "Teclado mecânico"
}

A resposta será algo como:

{
  "id": 1,
  "nome": "Teclado",
  "descricao": "Teclado mecânico"
}

Assim, conseguimos enviar e receber dados estruturados de forma padronizada.

Tratamento de erros

Em uma API, é essencial tratar erros de forma consistente. O Flask permite personalizar as respostas de erro usando o decorador @app.errorhandler. Podemos definir handlers para erros HTTP comuns, como 404 (Not Found) e 500 (Internal Server Error), além de erros personalizados.

Vamos implementar o tratamento para item não encontrado (404) e para erros genéricos:

from flask import Flask, request, jsonify

app = Flask(__name__)

itens = []
proximo_id = 1

# ... rotas anteriores ...

@app.route('/itens/<int:id>', methods=['GET'])
def obter_item(id):
    item = next((item for item in itens if item['id'] == id), None)
    if item is None:
        return jsonify({'erro': 'Item não encontrado'}), 404
    return jsonify(item)

@app.errorhandler(404)
def nao_encontrado(erro):
    return jsonify({'erro': 'Recurso não encontrado'}), 404

@app.errorhandler(500)
def erro_interno(erro):
    return jsonify({'erro': 'Erro interno do servidor'}), 500

Na rota obter_item, usamos a função next para encontrar o item com o ID especificado. Se não encontrar, retornamos uma mensagem de erro com status 404. Caso contrário, retornamos o item.

Os handlers de erro globais garantem que qualquer erro não tratado retorne uma resposta JSON em vez de uma página HTML padrão. Isso é importante para que o cliente saiba que houve um erro e possa lidar com isso de forma programática.

Além disso, podemos criar exceções personalizadas para situações específicas. Por exemplo, se tentarmos criar um item com dados inválidos, podemos lançar uma exceção que será capturada por um handler:

class DadosInvalidos(Exception):
    pass

@app.errorhandler(DadosInvalidos)
def dados_invalidos(erro):
    return jsonify({'erro': str(erro)}), 400

Dessa forma, em qualquer rota, podemos fazer raise DadosInvalidos('Mensagem específica') e o Flask responderá com status 400 e a mensagem em JSON.

Boas práticas e observações finais

Ao desenvolver APIs, é importante seguir algumas boas práticas:

  • Use nomes claros e consistentes: rotas e campos devem ser intuitivos.
  • Valide as entradas: sempre verifique se os dados recebidos são válidos antes de processá-los.
  • Retorne códigos de status apropriados: 200 para sucesso, 201 para criação, 400 para erros do cliente, 404 para não encontrado, 500 para erros do servidor.
  • Documente sua API: use ferramentas como Swagger/OpenAPI para descrever os endpoints.
  • Nunca confie em dados do cliente: sempre sanitize e valide.

Nesta aula, você construiu uma API simples com Flask, aprendendo a criar rotas, manipular JSON e tratar erros. Esses são os fundamentos para desenvolver APIs mais complexas, com autenticação, banco de dados e deploy em produção. Continue explorando e pratique implementando novos endpoints e melhorias.

Referências

Exercícios

  1. Implemente a rota PUT /itens/<int:id> para atualizar um item existente. Se o item não existir, retorne 404. Se os dados forem inválidos (sem nome), retorne 400.

    ✓ Resposta:
    @app.route('/itens/<int:id>', methods=['PUT'])
    def atualizar_item(id):
        global itens
        item = next((item for item in itens if item['id'] == id), None)
        if item is None:
            return jsonify({'erro': 'Item não encontrado'}), 404
        dados = request.get_json()
        if not dados or not 'nome' in dados:
            return jsonify({'erro': 'O campo nome é obrigatório'}), 400
        item['nome'] = dados.get('nome', item['nome'])
        item['descricao'] = dados.get('descricao', item.get('descricao', ''))
        return jsonify(item)
  2. Adicione uma rota DELETE /itens/<int:id> que remove o item da lista. Retorne 204 (No Content) em caso de sucesso e 404 se não existir.

    ✓ Resposta:
    @app.route('/itens/<int:id>', methods=['DELETE'])
    def deletar_item(id):
        global itens
        item = next((item for item in itens if item['id'] == id), None)
        if item is None:
            return jsonify({'erro': 'Item não encontrado'}), 404
        itens = [item for item in itens if item['id'] != id]
        return '', 204
  3. Crie um endpoint GET /itens que aceite um parâmetro de consulta q para filtrar itens pelo nome (busca parcial, case-insensitive).

    ✓ Resposta:
    @app.route('/itens', methods=['GET'])
    def listar_itens():
        q = request.args.get('q', '').lower()
        if q:
            filtrados = [item for item in itens if q in item['nome'].lower()]
            return jsonify(filtrados)
        return jsonify(itens)
  4. Implemente um handler para o método HTTP não permitido (405) retornando JSON com a mensagem "Método não permitido".

    ✓ Resposta:
    @app.errorhandler(405)
    def metodo_nao_permitido(erro):
        return jsonify({'erro': 'Método não permitido'}), 405
  5. Crie uma rota GET /itens/<int:id> que retorne o item, mas com um pequeno atraso simulado (ex.: time.sleep(1)) para testar o comportamento assíncrono. (Não se preocupe com performance agora.)

    ✓ Resposta:
    import time
    
    @app.route('/itens/<int:id>', methods=['GET'])
    def obter_item(id):
        time.sleep(1)
        item = next((item for item in itens if item['id'] == id), None)
        if item is None:
            return jsonify({'erro': 'Item não encontrado'}), 404
        return jsonify(item)