Como criar sua primeira API REST com Flask
Crie uma API REST completa com Flask: rotas, JSON, validação e tratamento de erros.
Por que Flask para APIs?
Flask é uma das escolhas mais populares para criar APIs REST em Python. Sendo um microframework, ele não impõe estrutura rígida — você monta a API do jeito que fizer sentido para o seu projeto. Além disso, a curva de aprendizado é suave: com poucas linhas você já tem uma API funcional.
Se você já seguiu os artigos anteriores sobre Flask, já conhece rotas e templates. Agora vamos trocar o HTML por JSON e construir uma API que pode ser consumida por qualquer cliente: frontend em React, app mobile, outro backend ou até scripts Python.
Configurando o ambiente
# crie a pasta do projeto
mkdir api-tarefas
cd api-tarefas
# crie e ative o ambiente virtual
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# instale o Flask
pip install flask
A API mais simples possível
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/status")
def status():
return jsonify({"status": "online", "versao": "1.0.0"})
if __name__ == "__main__":
app.run(debug=True)
Salve como app.py, rode com python app.py e acesse http://127.0.0.1:5000/api/status. Você verá o JSON de resposta. O jsonify converte o dicionário Python em uma resposta HTTP com Content-Type: application/json.
Planejando os endpoints
Antes de escrever código, planeje a API. Vamos criar uma API de tarefas (to-do list) com operações CRUD completas:
| Método | Endpoint | Ação | Status esperado |
|---|---|---|---|
| GET | /api/tarefas |
Listar todas as tarefas | 200 |
| GET | /api/tarefas/<id> |
Buscar uma tarefa | 200 ou 404 |
| POST | /api/tarefas |
Criar nova tarefa | 201 |
| PUT | /api/tarefas/<id> |
Atualizar tarefa completa | 200 ou 404 |
| DELETE | /api/tarefas/<id> |
Remover tarefa | 204 ou 404 |
Estrutura de dados em memória
Para focar na API sem a complexidade de um banco de dados, vamos usar uma lista em memória. Em um projeto real, você usaria SQLAlchemy, MongoDB ou outro banco.
from flask import Flask, jsonify, request
app = Flask(__name__)
# "banco de dados" em memoria
tarefas = [
{
"id": 1,
"titulo": "Estudar Flask",
"descricao": "Aprender a criar APIs REST",
"concluida": False
},
{
"id": 2,
"titulo": "Praticar Python",
"descricao": "Resolver exercicios no terminal",
"concluida": True
}
]
proximo_id = 3
GET — Listar todas as tarefas
@app.route("/api/tarefas", methods=["GET"])
def listar_tarefas():
return jsonify(tarefas), 200
Testando com curl:
curl http://127.0.0.1:5000/api/tarefas
Resposta:
[
{
"id": 1,
"titulo": "Estudar Flask",
"descricao": "Aprender a criar APIs REST",
"concluida": false
},
{
"id": 2,
"titulo": "Praticar Python",
"descricao": "Resolver exercicios no terminal",
"concluida": true
}
]
GET — Buscar uma tarefa pelo ID
@app.route("/api/tarefas/<int:tarefa_id>", methods=["GET"])
def buscar_tarefa(tarefa_id):
tarefa = next((t for t in tarefas if t["id"] == tarefa_id), None)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
return jsonify(tarefa), 200
O conversor <int:tarefa_id> garante que o Flask só aceita inteiros na URL. Se alguém acessar /api/tarefas/abc, recebe 404 automaticamente.
# tarefa existente
curl http://127.0.0.1:5000/api/tarefas/1
# tarefa inexistente (retorna 404)
curl http://127.0.0.1:5000/api/tarefas/999
POST — Criar nova tarefa
Aqui usamos request.get_json() para ler o corpo JSON enviado pelo cliente.
@app.route("/api/tarefas", methods=["POST"])
def criar_tarefa():
global proximo_id
dados = request.get_json()
# validacao basica
if not dados or "titulo" not in dados:
return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400
nova_tarefa = {
"id": proximo_id,
"titulo": dados["titulo"],
"descricao": dados.get("descricao", ""),
"concluida": dados.get("concluida", False)
}
tarefas.append(nova_tarefa)
proximo_id += 1
return jsonify(nova_tarefa), 201
Testando:
curl -X POST http://127.0.0.1:5000/api/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Criar API", "descricao": "Seguir o tutorial"}'
Note que retornamos status 201 (Created), não 200. Isso é uma boa prática REST — indica que um novo recurso foi criado.
PUT — Atualizar tarefa completa
O PUT substitui todos os campos do recurso. Se o cliente não enviar um campo, ele deve ser resetado ao valor padrão.
@app.route("/api/tarefas/<int:tarefa_id>", methods=["PUT"])
def atualizar_tarefa(tarefa_id):
tarefa = next((t for t in tarefas if t["id"] == tarefa_id), None)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
dados = request.get_json()
if not dados or "titulo" not in dados:
return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400
tarefa["titulo"] = dados["titulo"]
tarefa["descricao"] = dados.get("descricao", "")
tarefa["concluida"] = dados.get("concluida", False)
return jsonify(tarefa), 200
curl -X PUT http://127.0.0.1:5000/api/tarefas/1 \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudar Flask (atualizado)", "concluida": true}'
DELETE — Remover tarefa
@app.route("/api/tarefas/<int:tarefa_id>", methods=["DELETE"])
def deletar_tarefa(tarefa_id):
global tarefas
tarefa = next((t for t in tarefas if t["id"] == tarefa_id), None)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
tarefas = [t for t in tarefas if t["id"] != tarefa_id]
return "", 204
O status 204 (No Content) indica sucesso sem corpo na resposta. É o padrão para DELETE em APIs REST.
curl -X DELETE http://127.0.0.1:5000/api/tarefas/2
Validação de dados
Uma API robusta precisa validar os dados que recebe. Vamos criar uma função auxiliar para centralizar a validação:
def validar_tarefa(dados, obrigatorio=True):
erros = []
if dados is None:
return ["Corpo da requisicao deve ser JSON"]
if obrigatorio and "titulo" not in dados:
erros.append("Campo 'titulo' e obrigatorio")
if "titulo" in dados:
if not isinstance(dados["titulo"], str):
erros.append("Campo 'titulo' deve ser texto")
elif len(dados["titulo"].strip()) == 0:
erros.append("Campo 'titulo' nao pode ser vazio")
elif len(dados["titulo"]) > 200:
erros.append("Campo 'titulo' deve ter no maximo 200 caracteres")
if "concluida" in dados and not isinstance(dados["concluida"], bool):
erros.append("Campo 'concluida' deve ser true ou false")
return erros
Usando na rota POST:
@app.route("/api/tarefas", methods=["POST"])
def criar_tarefa():
global proximo_id
dados = request.get_json()
erros = validar_tarefa(dados)
if erros:
return jsonify({"erros": erros}), 400
nova_tarefa = {
"id": proximo_id,
"titulo": dados["titulo"].strip(),
"descricao": dados.get("descricao", "").strip(),
"concluida": dados.get("concluida", False)
}
tarefas.append(nova_tarefa)
proximo_id += 1
return jsonify(nova_tarefa), 201
Tratamento de erros global
O Flask permite capturar erros globalmente com @app.errorhandler. Isso garante que qualquer erro retorne JSON, não HTML:
@app.errorhandler(404)
def nao_encontrado(erro):
return jsonify({"erro": "Recurso nao encontrado"}), 404
@app.errorhandler(405)
def metodo_nao_permitido(erro):
return jsonify({"erro": "Metodo HTTP nao permitido"}), 405
@app.errorhandler(500)
def erro_interno(erro):
return jsonify({"erro": "Erro interno do servidor"}), 500
@app.errorhandler(400)
def requisicao_invalida(erro):
return jsonify({"erro": "Requisicao invalida"}), 400
Sem esses handlers, o Flask retorna páginas HTML de erro padrão — o que não faz sentido para uma API.
Organizando com Blueprints
Conforme a API cresce, manter tudo em um único arquivo fica insustentável. Blueprints permitem separar rotas em módulos.
Estrutura de pastas
api-tarefas/
├── app/
│ ├── __init__.py
│ ├── tarefas/
│ │ ├── __init__.py
│ │ └── routes.py
│ └── errors.py
├── run.py
└── requirements.txt
app/tarefas/routes.py
from flask import Blueprint, jsonify, request
tarefas_bp = Blueprint("tarefas", __name__, url_prefix="/api/tarefas")
tarefas = []
proximo_id = 1
@tarefas_bp.route("", methods=["GET"])
def listar():
return jsonify(tarefas), 200
@tarefas_bp.route("/<int:tarefa_id>", methods=["GET"])
def buscar(tarefa_id):
tarefa = next((t for t in tarefas if t["id"] == tarefa_id), None)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
return jsonify(tarefa), 200
@tarefas_bp.route("", methods=["POST"])
def criar():
global proximo_id
dados = request.get_json()
if not dados or "titulo" not in dados:
return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400
nova_tarefa = {
"id": proximo_id,
"titulo": dados["titulo"],
"descricao": dados.get("descricao", ""),
"concluida": False
}
tarefas.append(nova_tarefa)
proximo_id += 1
return jsonify(nova_tarefa), 201
app/init.py
from flask import Flask
def create_app():
app = Flask(__name__)
from app.tarefas.routes import tarefas_bp
app.register_blueprint(tarefas_bp)
return app
run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
Exemplo completo: API funcional em um arquivo
Para facilitar o aprendizado, aqui está a API completa em um único arquivo:
from flask import Flask, jsonify, request
app = Flask(__name__)
tarefas = [
{"id": 1, "titulo": "Estudar Flask", "descricao": "Criar uma API REST", "concluida": False},
{"id": 2, "titulo": "Praticar Python", "descricao": "Exercicios diarios", "concluida": True},
]
proximo_id = 3
def encontrar_tarefa(tarefa_id):
return next((t for t in tarefas if t["id"] == tarefa_id), None)
@app.route("/api/tarefas", methods=["GET"])
def listar_tarefas():
return jsonify(tarefas), 200
@app.route("/api/tarefas/<int:tarefa_id>", methods=["GET"])
def buscar_tarefa(tarefa_id):
tarefa = encontrar_tarefa(tarefa_id)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
return jsonify(tarefa), 200
@app.route("/api/tarefas", methods=["POST"])
def criar_tarefa():
global proximo_id
dados = request.get_json()
if not dados or "titulo" not in dados:
return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400
nova_tarefa = {
"id": proximo_id,
"titulo": dados["titulo"],
"descricao": dados.get("descricao", ""),
"concluida": dados.get("concluida", False)
}
tarefas.append(nova_tarefa)
proximo_id += 1
return jsonify(nova_tarefa), 201
@app.route("/api/tarefas/<int:tarefa_id>", methods=["PUT"])
def atualizar_tarefa(tarefa_id):
tarefa = encontrar_tarefa(tarefa_id)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
dados = request.get_json()
if not dados or "titulo" not in dados:
return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400
tarefa["titulo"] = dados["titulo"]
tarefa["descricao"] = dados.get("descricao", "")
tarefa["concluida"] = dados.get("concluida", False)
return jsonify(tarefa), 200
@app.route("/api/tarefas/<int:tarefa_id>", methods=["DELETE"])
def deletar_tarefa(tarefa_id):
global tarefas
tarefa = encontrar_tarefa(tarefa_id)
if tarefa is None:
return jsonify({"erro": "Tarefa nao encontrada"}), 404
tarefas = [t for t in tarefas if t["id"] != tarefa_id]
return "", 204
@app.errorhandler(404)
def nao_encontrado(erro):
return jsonify({"erro": "Recurso nao encontrado"}), 404
@app.errorhandler(500)
def erro_interno(erro):
return jsonify({"erro": "Erro interno do servidor"}), 500
if __name__ == "__main__":
app.run(debug=True)
Testando a API completa com curl
# listar todas as tarefas
curl http://127.0.0.1:5000/api/tarefas
# buscar tarefa por id
curl http://127.0.0.1:5000/api/tarefas/1
# criar nova tarefa
curl -X POST http://127.0.0.1:5000/api/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudar APIs", "descricao": "Ler a documentacao"}'
# atualizar tarefa
curl -X PUT http://127.0.0.1:5000/api/tarefas/1 \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudar Flask (feito)", "concluida": true}'
# deletar tarefa
curl -X DELETE http://127.0.0.1:5000/api/tarefas/2
Resumo
| Conceito | O que aprendemos |
|---|---|
jsonify |
Converte dicionários Python em respostas JSON |
request.get_json() |
Lê o corpo JSON da requisição |
| Status codes | 200, 201, 204 para sucesso; 400, 404 para erros |
| Validação | Verificar dados antes de processar |
| Error handlers | Garantir que erros retornem JSON |
| Blueprints | Organizar rotas em módulos separados |
Com essa base sólida, você já consegue criar APIs REST funcionais com Flask. Nos próximos artigos vamos adicionar autenticação com JWT e testes automatizados.