Programador Leigo
Flask 12 min leitura 15 MAR 2026

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.

Continue lendo

Compartilhar