Construindo uma API RESTful em Python com FastAPI

Quando você começa a criar APIs, muitos tutoriais pulam direto para o código. Entretanto, entender por que cada decisão é tomada torna tudo mais simples de manter, testar e evoluir. Este é um artigo para quem já tem uma noção de programação em Python mas, se você está iniciando sua jornada com a programação em Python, poderá ler esses dois artigos antes, onde damos os primeiros passos em Python e depois entendemos melhor criando nosso primeiro programa. Mas se você já estiver pronto, vamos adiante!

Então, vamos construir uma API de Produtos passo a passo, com camadas bem definidas, validação robusta e documentação automática.

Se você está começando no mundo das APIs, este guia foi feito especialmente para você. Assim, vamos construir uma API RESTful de Produtos com Python, usando o framework FastAPI e um banco de dados SQLite. Por fim, você entenderá o que é um CRUD como API, como cada camada se encaixa e como testar sua aplicação com segurança.

Ao final, terá um projeto completo e funcional, pronto para evoluir com autenticação, filtros e testes automatizados.

O que é uma API RESTful e como o CRUD se encaixa nela?

Antes de começar a codar, é importante entender o conceito.
Uma API RESTful é uma interface que permite que sistemas diferentes conversem entre si usando o protocolo HTTP — o mesmo da web. Ela se baseia em padrões claros: recursos, métodos e status.

Por exemplo, se o recurso é “produtos”, nossa API terá URLs como:

GET /produtos
POST /produtos
PUT /produtos/{id}
DELETE /produtos/{id}

Essas quatro operações formam o famoso CRUD, que representa:

LetraAçãoDescrição
CCreateCriar registros (POST)
RReadLer/listar registros (GET)
UUpdateAtualizar registros (PUT/PATCH)
DDeleteExcluir registros (DELETE)

Ao longo deste artigo, cada parte do código será associada a uma dessas letras.
Assim, você saberá claramente em que etapa do CRUD está trabalhando. Mas, se você prefere antes construir uma aplicação com essas operações para entender primeiro a camada de persistência, leia antes esse artigo sobre CRUD com Python.

O que você vai construir

Uma API RESTful de Produtos com:

  • Camadas claras: Rotas (FastAPI) → Serviço (regras) → Repositório (SQLite).
  • Contratos HTTP padronizados: JSON, status codes e cabeçalhos previsíveis.
  • Validação de dados: feita automaticamente pelo Pydantic.
  • Documentação interativa: Swagger/OpenAPI e permitindo teste dos endpoints pelo navegador.
  • CRUD completo: criar, listar, atualizar e excluir produtos persistindo em SQLite.

Preparando o ambiente

Para começar, você precisa do Python instalado. Então, se não tiver o Pyton ainda instalado, siga o artigo sobre instalação de Python.
Depois, abra o terminal e crie um ambiente virtual isolado — isso evita conflitos de dependências entre projetos. Se desejar saber mais sobre isso, veja esse artigo sobre ambientes de execução Python isolados.

python -m venv venv

Ative o ambiente:

  • No Linux/macOS: source venv/bin/activate
  • No Windows (PowerShell): venv\Scripts\activate

Com o ambiente ativo, instale as bibliotecas que usaremos:

pip install fastapi uvicorn pydantic

O fastapi é o framework principal.
O uvicorn é o servidor ASGI que executará a aplicação.
E o pydantic é a biblioteca que validará automaticamente os dados das requisições.

💡 Dica: se preferir, crie um arquivo requirements.txt e coloque dentro:

fastapi
uvicorn
pydantic

Depois, execute:

pip install -r requirements.txt

Assim, fica mais fácil compartilhar o projeto com outras pessoas.

Testando requisições: Terminal, cURL ou Postman

Durante o desenvolvimento, você poderá testar as rotas da API de várias maneiras:

  • Usando o navegador: o FastAPI já traz o Swagger em /docs.
  • Usando o terminal: com o comando curl (nativo no Linux e macOS).
  • Usando o Postman: ideal para quem está no Windows ou prefere uma interface gráfica.

O Postman é gratuito e permite salvar coleções de requisições, ajustar cabeçalhos e visualizar respostas de forma organizada.

Estrutura do projeto

Vamos organizar nosso projeto desta forma:

api_produtos/
├─ main.py                 # Rotas (FastAPI)
├─ models.py               # Modelos Pydantic (validação de entrada e saída)
├─ database.py             # Conexão com SQLite
├─ repository.py           # Repositório (operações SQL)
└─ services.py             # Regras de negócio (camada de serviço)

Cada camada tem uma função clara:

  • Rotas: expõem endpoints HTTP.
  • Serviço: contém regras de negócio.
  • Repositório: acessa o banco de dados.
  • Modelos (Pydantic): validam e estruturam os dados.

Por que FastAPI?

FastAPI é um framework moderno, rápido e intuitivo. Esse artigo só apresenta uma “beiradinha” do que ele é capaz, mas será ótimo para você ter uma noção se ainda não o conhece!
Esse framework também usa type hints para validar automaticamente seus dados e gerar documentação interativa.

Portanto, ele:

  • Integra o Pydantic de forma nativa, simplificando contratos.
  • Valida automaticamente seus dados com base nos modelos.
  • Gera documentação OpenAPI (Swagger em /docs e Redoc em /redoc).
  • Roda sobre ASGI, oferecendo excelente desempenho com pouco boilerplate.

Assim, você escreve menos código, com mais segurança e velocidade.

Onde entra o Pydantic?

O Pydantic define os “Models” ou modelos de dados (esquemas) de validação. Desse modo, ele:

  • Garante tipos corretos (str, float, int…).
  • Aplica regras de negócio simples (ex.: preco > 0).
  • Gera schemas JSON aproveitados pelo Swagger.

Consequentemente, erros são barrados na borda da API e os contratos ficam padronizados.

Sobre os “Models”: eles são iguais aos de um MVC?

Essa é uma dúvida comum — e muito válida.

No MVC (Model-View-Controller) tradicional, o Model representa a entidade de negócio e também é responsável por se comunicar com o banco de dados.
Já no FastAPI, usamos dois tipos de Model com responsabilidades diferentes:

  1. Modelos Pydantic: descrevem como os dados entram e saem da API (validação e serialização).
  2. Modelos do domínio (ou ORM): representam a entidade no banco (quando usamos ORM, como SQLAlchemy).

Mesmo parecendo duplicidade, essa separação é intencional. Porque, ela garante isolamento entre camadas, ou seja, se o banco mudar, a API continua com os mesmos contratos externos. Portanto, o sistema fica mais robusto e fácil de manter.

Criando o CRUD de Produtos passo a passo

Agora vamos então construir nosso projeto. Cada parte será associada à letra correspondente do CRUD.

1. Modelos — models.py (base do domínio e validação de dados)

Essa parte, como dito ali acima, define como os dados chegam e saem da API. Então crie o arquivo models.py e insira esse código:

from pydantic import BaseModel, Field, field_validator
from typing import Optional

class ProdutoBase(BaseModel):
    nome: str = Field(..., example="Mouse sem fio")
    preco: float = Field(..., gt=0, example=99.90)
    descricao: Optional[str] = Field(None, example="Mouse óptico USB com alta precisão")

    @field_validator("nome")
    def nome_nao_vazio(cls, v):
        if not v or not v.strip():
            raise ValueError("nome não pode ser vazio")
        return v.strip()

class ProdutoCriar(ProdutoBase):
    pass

class ProdutoAtualizar(ProdutoBase):
    pass

class Produto(ProdutoBase):
    id: int

Observe que aproveitamos para definiros tipos que extendem da classe base. Fazemos isso para separar e especificar os tipos conforme as suas responsabilidades.

2. Banco de dados — database.py

Nessa etapa, podemos já começar definindo um jeito padrão de “criamos e gerenciamos a conexão com o SQLite”. Então crie o arquivo database.py e adicione esse código:

import sqlite3
from pathlib import Path
from contextlib import contextmanager

CAMINHO_BANCO = Path("produtos.sqlite")  # Definimos aqui uma constante com o caminho do arquivo do banco de dados.

# essa função sempre criará nossa conexão com banco de dados
def obter_conexao():
    con = sqlite3.connect(CAMINHO_BANCO, check_same_thread=False)
    con.row_factory = sqlite3.Row
    return con

@contextmanager
def conexao_gerenciada():
    con = obter_conexao()
    try:
        yield con
        con.commit()
    except:
        con.rollback()
        raise
    finally:
        con.close()

def inicializar_banco():
    with conexao_gerenciada() as con:
        con.execute("""
            CREATE TABLE IF NOT EXISTS produtos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                nome TEXT NOT NULL,
                preco REAL NOT NULL,
                descricao TEXT
            );
        """)

Este código configura uma maneira robusta e segura de interagir com um banco de dados SQLite (produtos.sqlite). Ele define como obter a conexão, usa um gerenciador de contexto (conexao_gerenciada) para garantir que a conexão seja sempre aberta, comitada (ou desfeita) e fechada corretamente, e possui uma função para criar a tabela inicial de produtos.

Observação: row_factory=sqlite3.Row facilita transformar linhas em dict

3. Repositório — repository.py

Aqui implementamos as operações diretas no banco de dados (sem regras de negócio). Veja que temos todos os métodos de recuperação e persistência de dados.

from typing import List, Optional
from models import Produto
from database import conexao_gerenciada

class RepositorioProduto:
    # R READ
    def listar(self) -> List[Produto]:
        with conexao_gerenciada() as con:
            cur = con.execute("SELECT * FROM produtos ORDER BY id")
            return [Produto(**dict(row)) for row in cur.fetchall()]

    # R READ
    def obter_por_id(self, produto_id: int) -> Optional[Produto]:
        with conexao_gerenciada() as con:
            cur = con.execute("SELECT * FROM produtos WHERE id = ?", (produto_id,))
            row = cur.fetchone()
            return Produto(**dict(row)) if row else None

    # C - CREATE
    def criar(self, nome: str, preco: float, descricao: Optional[str]) -> Produto:
        with conexao_gerenciada() as con:
            cur = con.execute(
                "INSERT INTO produtos (nome, preco, descricao) VALUES (?, ?, ?)",
                (nome, preco, descricao),
            )
            novo_id = cur.lastrowid
            return Produto(id=novo_id, nome=nome, preco=preco, descricao=descricao)

    # U - UPDATE
    def atualizar(self, produto_id: int, nome: str, preco: float, descricao: Optional[str]) -> Optional[Produto]:
        with conexao_gerenciada() as con:
            cur = con.execute("""
                UPDATE produtos
                   SET nome = ?, preco = ?, descricao = ?
                 WHERE id = ?
            """, (nome, preco, descricao, produto_id))
            if cur.rowcount == 0:
                return None
            return Produto(id=produto_id, nome=nome, preco=preco, descricao=descricao)

    # D - DELETE
    def excluir(self, produto_id: int) -> bool:
        with conexao_gerenciada() as con:
            cur = con.execute("DELETE FROM produtos WHERE id = ?", (produto_id,))
            return cur.rowcount > 0

Note que repositório não decide regras; ele apenas lê/escreve. Portanto, seu teste é direto e seu uso é previsível.

4. Serviço — services.py

Agora que temos o respoitorio, precisamos de serviços. Um Serviço centraliza as regras de negócio e o tratamento de erros. Além disso, de forma simples, rota fala com o serviço, que fala com o repositório.

from typing import List
from models import Produto, ProdutoCriar, ProdutoAtualizar
from repository import RepositorioProduto

class ErroDeNegocio(Exception):
    """Erros de domínio/regra de negócio."""

class ServicoProduto:
    def __init__(self, repositorio: RepositorioProduto):
        self.repositorio = repositorio

    def listar(self) -> List[Produto]:
        return self.repositorio.listar()

    def obter(self, produto_id: int) -> Produto:
        produto = self.repositorio.obter_por_id(produto_id)
        if not produto:
            raise ErroDeNegocio("produto_nao_encontrado")
        return produto

    def criar(self, dto: ProdutoCriar) -> Produto:
        return self.repositorio.criar(dto.nome, dto.preco, dto.descricao)

    def atualizar(self, produto_id: int, dto: ProdutoAtualizar) -> Produto:
        atualizado = self.repositorio.atualizar(produto_id, dto.nome, dto.preco, dto.descricao)
        if not atualizado:
            raise ErroDeNegocio("produto_nao_encontrado")
        return atualizado

    def excluir(self, produto_id: int) -> None:
        ok = self.repositorio.excluir(produto_id)
        if not ok:
            raise ErroDeNegocio("produto_nao_encontrado")

Detalhe Importante: Onde Colocamos as Regras?

Em nosso projeto, optamos por uma abordagem mais simples:

  • A Camada de Serviço é onde colocamos as regras de domínio mais básicas (o “como” e “porquê” das ações).

Mas, saiba que…

Em arquiteturas mais avançadas (como a Hexagonal ou Arquitetura Limpa), as regras de negócio são isoladas nas próprias Entidades de Domínio e Objetos de Valor. Isso é feito para:

  • Isolar estrategicamente cada parte.
  • Garantir melhor manutenção e testabilidade.

Para Você Agora:

Não se preocupe com a complexidade dessas arquiteturas mais avançadas por enquanto. Mantenha o foco no Serviço.

Dica para Depuração:

Se um erro de execução ocorrer (e não for erro de digitação), olhar para a Camada de Serviço — como ela invoca seus métodos e quem ela chama (ex: o Repositório) — é o seu melhor ponto de partida para encontrar e corrigir o problema.

5. Rotas — main.py

Agoras por meio das rotas, definimos os endpoints HTTP da API usando FastAPI. Então crie o arquivo main.py e adicione esté código:

from fastapi import FastAPI, HTTPException, Depends
from typing import List
from models import Produto, ProdutoCriar, ProdutoAtualizar
from database import inicializar_banco
from repository import RepositorioProduto
from services import ServicoProduto, ErroDeNegocio

app = FastAPI(
    title="API de Produtos — FastAPI + Serviço + Repositório",
    version="1.0.0",
    description="Exemplo didático de API RESTful com SQLite e camadas",
)

def obter_servico_produto() -> ServicoProduto:
    return ServicoProduto(RepositorioProduto())

@app.on_event("startup")
def ao_iniciar():
    inicializar_banco()

@app.get("/", tags=["Raiz"])
def raiz():
    return {"mensagem": "API de Produtos — consulte /docs para testar no Swagger"}

@app.get("/produtos", response_model=List[Produto], tags=["Produtos"])
def listar_produtos(servico: ServicoProduto = Depends(obter_servico_produto)):
    return servico.listar()

@app.get("/produtos/{produto_id}", response_model=Produto, tags=["Produtos"])
def obter_produto(produto_id: int, servico: ServicoProduto = Depends(obter_servico_produto)):
    try:
        return servico.obter(produto_id)
    except ErroDeNegocio:
        raise HTTPException(status_code=404, detail="Produto não encontrado")

@app.post("/produtos", response_model=Produto, status_code=201, tags=["Produtos"])
def criar_produto(dto: ProdutoCriar, servico: ServicoProduto = Depends(obter_servico_produto)):
    return servico.criar(dto)

@app.put("/produtos/{produto_id}", response_model=Produto, tags=["Produtos"])
def atualizar_produto(produto_id: int, dto: ProdutoAtualizar, servico: ServicoProduto = Depends(obter_servico_produto)):
    try:
        return servico.atualizar(produto_id, dto)
    except ErroDeNegocio:
        raise HTTPException(status_code=404, detail="Produto não encontrado")

@app.delete("/produtos/{produto_id}", status_code=204, tags=["Produtos"])
def excluir_produto(produto_id: int, servico: ServicoProduto = Depends(obter_servico_produto)):
    try:
        servico.excluir(produto_id)
    except ErroDeNegocio:
        raise HTTPException(status_code=404, detail="Produto não encontrado")

Executando o servidor

Agora já temos um CRUD completo como API e que pode operar sobre nosso banco de dados. Então, para testar, primeiro rode o comando:

uvicorn main:app --reload

O parâmetro --reload recarrega automaticamente a API sempre que você salvar um arquivo.
Por padrão, ela ficará disponível em:
👉 http://127.0.0.1:8000

E a documentação Swagger em:
👉 http://127.0.0.1:8000/docs

Testando a API

Você pode testar:

  • No Swagger do próprio navegador.
  • No Postman.
  • Ou usando cURL pelo terminal (como mostramos antes).

Cada teste representa uma das operações do CRUD:
Criar (POST), Ler (GET), Atualizar (PUT) e Excluir (DELETE).

Testar no Swagger (recomendado)

Acesse http://127.0.0.1:8000/docs. Em seguida, clique no endpoint → Try it out → preencha → Execute.
Você verá request, response, status e schema — tudo claro e interativo.

Por exemplo, vá até POST / produtos e clique em Try it out:

Observe que um campo com o corpo da requisição é exibido com valores de exemplo, que definimos lá no início ao construir a Model do Pydantic. Sendo assim, você pode clicar em ¨ Execute” e esses dados serão sumetidos. Ou ainda, poderá primeiro alterá-los antes de executar e por fim “executar”.

Depois de executar, você vai perceber uma resposta imediata logo abaixo com o id do registro criado. Além disso, observe que lá no terminal, onde você iniciou o uvicorn, um log é apresentado mostrando cada solicitação feita ao servidor e suas respostas.

você também deve ter notado a criação do arquivo produtos.sqlite no diretório do projeto.

Testes rápidos por cURL

Criar:

curl -X POST http://127.0.0.1:8000/produtos \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"nome": "Cadeira Gamer", "preco": 1299.99, "descricao": "Ergonômica e reclinável"}'

Listar:

curl -H "Accept: application/json" http://127.0.0.1:8000/produtos

Atualizar:

curl -X PUT http://127.0.0.1:8000/produtos/1 \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"nome": "Cadeira Gamer Pro", "preco": 1499.90, "descricao": "Novo encosto lombar"}'

Excluir:

curl -X DELETE -i http://127.0.0.1:8000/produtos/1

Conclusão

Com poucos arquivos e uma estrutura bem pensada, você acabou de criar uma API RESTful completa com FastAPI.
Mais importante: agora entende o papel de cada camada e como elas se relacionam.

Separar responsabilidades pode parecer repetição, mas é o que permite um código mais limpo e sustentável.
Se o dia de amanhã exigir trocar o banco de dados ou adicionar autenticação, você já terá uma base sólida para isso.

Veja também

Referências

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima