SQLModel com FastAPI: APIs Tipadas com Banco de Dados
Aprenda a usar SQLModel com FastAPI para criar APIs Python tipadas, com modelos reutilizáveis, SQLite, sessões, CRUD, testes e boas práticas.
Criar uma API em Python ficou muito mais simples com FastAPI, Pydantic e tipagem moderna. O ponto em que muitos projetos ainda ficam confusos é a camada de banco de dados: você define um modelo Pydantic para entrada, outro para resposta, outro modelo SQLAlchemy para tabela, repete campos, esquece validações e acaba mantendo três versões da mesma entidade.
O SQLModel tenta reduzir essa duplicação. Ele combina ideias do Pydantic e do SQLAlchemy para que o mesmo modelo descreva dados validados, objetos Python e tabelas relacionais. Para uma API pequena ou média, especialmente com FastAPI, isso deixa o código mais direto sem abandonar um ORM sólido por baixo.
Neste guia, vamos montar uma API de tarefas com SQLModel, FastAPI e SQLite. O objetivo não é criar um framework mágico, e sim mostrar um caminho profissional para projetos de portfólio, testes técnicos e aplicações internas. Se você ainda está revisando a base, leia também criando API com FastAPI, SQLAlchemy 2.0, testes com pytest e Docker em projetos Python.
Quando SQLModel faz sentido
SQLModel funciona melhor quando seu projeto tem entidades relativamente claras e você quer aproveitar tipagem de ponta a ponta. Alguns cenários comuns:
| Cenário | Por que SQLModel ajuda |
|---|---|
| API CRUD com FastAPI | modelos de entrada, saída e tabela ficam próximos |
| Projeto de portfólio | menos boilerplate e mais foco em regra de negócio |
| Sistema interno | validação e persistência ficam fáceis de explicar |
| Teste técnico | código compacto, legível e tipado |
| MVP com SQLite/PostgreSQL | começa simples e evolui sem trocar toda a camada |
Ele não substitui SQLAlchemy em todos os casos. Se você precisa de mapeamentos complexos, herança avançada, queries muito específicas ou um domínio grande, SQLAlchemy puro pode dar mais controle. Mas para APIs de produto, catálogos, tarefas, CRM leve, dashboards internos e integrações, SQLModel costuma ser suficiente e produtivo.
Preparando o ambiente
Crie um projeto novo:
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn sqlmodel pytest httpx
Com uv, o fluxo fica assim:
uv init api-tarefas-sqlmodel
cd api-tarefas-sqlmodel
uv add fastapi uvicorn sqlmodel
uv add --dev pytest httpx
Uma estrutura pequena e limpa pode começar com estes arquivos:
api-tarefas-sqlmodel/
app/
__init__.py
database.py
main.py
models.py
tests/
test_tarefas.py
pyproject.toml
Manter models.py, database.py e main.py separados ajuda a não transformar a API em um arquivo gigante. Também facilita testes, porque você consegue trocar o banco durante a suíte.
Criando os modelos
No SQLModel, você pode declarar um modelo base com campos compartilhados e depois especializar para tabela, criação e leitura. Crie app/models.py:
from sqlmodel import Field, SQLModel
class TarefaBase(SQLModel):
titulo: str = Field(min_length=3, max_length=120)
descricao: str | None = Field(default=None, max_length=500)
concluida: bool = False
class Tarefa(TarefaBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class TarefaCreate(TarefaBase):
pass
class TarefaRead(TarefaBase):
id: int
Esse padrão evita expor detalhes internos na entrada. TarefaCreate não aceita id, porque o banco gera esse valor. TarefaRead exige id, porque a resposta de uma tarefa persistida deve ter identificador. Tarefa é o modelo de tabela real, marcado com table=True.
Você poderia usar apenas uma classe para tudo, mas separar criação, leitura e tabela deixa a API mais segura. Em sistemas reais, é comum ter campos que existem no banco mas não devem vir do cliente, como created_at, updated_at, user_id, hashed_password ou flags administrativas.
Configurando o banco
Agora crie app/database.py:
from collections.abc import Generator
from sqlmodel import Session, SQLModel, create_engine
DATABASE_URL = "sqlite:///tarefas.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
def criar_banco() -> None:
SQLModel.metadata.create_all(engine)
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
O check_same_thread=False é necessário no SQLite quando a aplicação web pode acessar a conexão em threads diferentes. Em produção com PostgreSQL, esse argumento não entra. O importante é centralizar o engine e a dependência de sessão em um lugar só.
Para um projeto de portfólio, documente no README que SQLite foi escolhido pela simplicidade local. Se a vaga pede backend mais robusto, mostre como trocar para PostgreSQL via variável de ambiente e Docker Compose.
Criando a API com FastAPI
Crie app/main.py:
from fastapi import Depends, FastAPI, HTTPException, status
from sqlmodel import Session, select
from app.database import criar_banco, get_session
from app.models import Tarefa, TarefaCreate, TarefaRead
app = FastAPI(title="API de Tarefas com SQLModel")
@app.on_event("startup")
def on_startup() -> None:
criar_banco()
@app.post("/tarefas", response_model=TarefaRead, status_code=status.HTTP_201_CREATED)
def criar_tarefa(
payload: TarefaCreate,
session: Session = Depends(get_session),
) -> Tarefa:
tarefa = Tarefa.model_validate(payload)
session.add(tarefa)
session.commit()
session.refresh(tarefa)
return tarefa
@app.get("/tarefas", response_model=list[TarefaRead])
def listar_tarefas(session: Session = Depends(get_session)) -> list[Tarefa]:
statement = select(Tarefa).order_by(Tarefa.id.desc())
return list(session.exec(statement).all())
@app.get("/tarefas/{tarefa_id}", response_model=TarefaRead)
def buscar_tarefa(tarefa_id: int, session: Session = Depends(get_session)) -> Tarefa:
tarefa = session.get(Tarefa, tarefa_id)
if tarefa is None:
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
return tarefa
Esse código já entrega documentação automática em /docs, validação de entrada, serialização de resposta e persistência. A linha Tarefa.model_validate(payload) transforma o modelo de criação em modelo de tabela, preservando validações. Depois de commit(), refresh() atualiza o objeto com o id gerado pelo banco.
Rode a API:
uvicorn app.main:app --reload
Teste com curl:
curl -X POST http://127.0.0.1:8000/tarefas \
-H 'Content-Type: application/json' \
-d '{"titulo": "Revisar portfólio", "descricao": "Adicionar testes e README"}'
curl http://127.0.0.1:8000/tarefas
Atualização e remoção
Para completar o CRUD, adicione modelos e rotas de atualização. Em models.py:
class TarefaUpdate(SQLModel):
titulo: str | None = Field(default=None, min_length=3, max_length=120)
descricao: str | None = Field(default=None, max_length=500)
concluida: bool | None = None
Depois, em main.py:
from app.models import Tarefa, TarefaCreate, TarefaRead, TarefaUpdate
@app.patch("/tarefas/{tarefa_id}", response_model=TarefaRead)
def atualizar_tarefa(
tarefa_id: int,
payload: TarefaUpdate,
session: Session = Depends(get_session),
) -> Tarefa:
tarefa = session.get(Tarefa, tarefa_id)
if tarefa is None:
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
dados = payload.model_dump(exclude_unset=True)
tarefa.sqlmodel_update(dados)
session.add(tarefa)
session.commit()
session.refresh(tarefa)
return tarefa
@app.delete("/tarefas/{tarefa_id}", status_code=status.HTTP_204_NO_CONTENT)
def remover_tarefa(tarefa_id: int, session: Session = Depends(get_session)) -> None:
tarefa = session.get(Tarefa, tarefa_id)
if tarefa is None:
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
session.delete(tarefa)
session.commit()
O detalhe importante é exclude_unset=True. Em um PATCH, o cliente pode enviar apenas {"concluida": true}. Sem esse cuidado, campos não enviados poderiam virar None e apagar dados sem querer.
Testando sem depender do banco real
FastAPI permite sobrescrever dependências durante os testes. Isso é ótimo para usar banco em memória ou arquivo temporário. Em tests/test_tarefas.py:
from collections.abc import Generator
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool
from app.database import get_session
from app.main import app
def criar_session_teste() -> Generator[Session, None, None]:
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
app.dependency_overrides[get_session] = criar_session_teste
client = TestClient(app)
def test_cria_e_lista_tarefa() -> None:
resposta = client.post("/tarefas", json={"titulo": "Escrever teste"})
assert resposta.status_code == 201
assert resposta.json()["titulo"] == "Escrever teste"
lista = client.get("/tarefas")
assert lista.status_code == 200
assert len(lista.json()) >= 1
Esse teste valida a API pelo mesmo caminho que um cliente usaria: HTTP, JSON, validação, sessão e resposta. Para testes técnicos, isso costuma impressionar mais do que uma coleção de funções sem integração real.
Em projetos maiores, organize fixtures do pytest, limpe overrides ao fim de cada teste e separe testes unitários de testes de API. Veja o guia de testes com pytest para estruturar isso melhor.
Boas práticas para produção
SQLModel deixa a primeira versão rápida, mas produção exige algumas decisões explícitas:
- use migrações com Alembic quando o schema começar a mudar;
- configure PostgreSQL ou outro banco persistente em vez de SQLite para múltiplos usuários;
- evite misturar regra de negócio complexa dentro das rotas;
- registre logs com contexto de requisição e erro;
- valide paginação, filtros e limites de consulta;
- proteja endpoints com autenticação quando houver dados de usuário;
- adicione testes para erro 404, payload inválido e atualização parcial.
Também pense em observabilidade. Uma API com banco pode falhar por query lenta, conexão esgotada, bloqueio, falta de índice ou payload inesperado. O artigo de OpenTelemetry com Python mostra como começar a medir latência e erro por endpoint.
SQLModel, SQLAlchemy ou Django ORM?
Uma comparação prática:
| Opção | Melhor quando |
|---|---|
| SQLModel | FastAPI, CRUD tipado, MVP, portfólio, APIs pequenas e médias |
| SQLAlchemy 2.0 | domínio complexo, controle fino, queries avançadas, múltiplos padrões |
| Django ORM | projeto Django completo, admin, autenticação e convenções integradas |
Se o projeto já é Django, use Django ORM. Se a API é FastAPI e você quer produtividade com tipagem, SQLModel é uma ótima escolha. Se o banco é o centro do sistema e as consultas são sofisticadas, SQLAlchemy puro ainda pode ser melhor.
Para performance extrema ou serviços de alta concorrência, Python continua útil, mas pode conviver com outras linguagens. Um backend pode usar FastAPI para produto e automação, enquanto serviços específicos de fila ou gateway usam Go para concorrência e binários simples. O importante é escolher por gargalo real, não por moda.
Projeto de portfólio recomendado
Um bom projeto para demonstrar SQLModel é uma API de candidatura a vagas:
Candidato: nome, e-mail, senioridade e área de interesse;Vaga: título, empresa, tecnologias e modalidade;Candidatura: relação entre candidato, vaga, status e data;- filtros por tecnologia, senioridade e status;
- testes de criação, listagem, atualização e erro;
- README com decisões de modelagem, comandos e exemplos
curl.
Esse projeto conversa diretamente com vagas Python no Brasil, projetos de portfólio Python e teste técnico Python. Ele mostra backend, banco, validação, testes e documentação em um escopo pequeno o suficiente para terminar.
Conclusão
SQLModel é uma ponte interessante entre a produtividade do FastAPI, a validação do Pydantic e a maturidade do SQLAlchemy. Ele reduz repetição, melhora a leitura dos modelos e permite criar APIs tipadas com banco relacional sem começar por uma arquitetura pesada.
Use SQLModel quando o domínio ainda é simples, mas você quer código profissional: modelos separados para criação e leitura, sessões controladas, testes com dependência sobrescrita, PATCH seguro e documentação clara. Se o projeto crescer, você ainda terá conceitos compatíveis com SQLAlchemy e poderá evoluir com migrações, repositórios, serviços e observabilidade.
Para quem busca vaga backend Python, esse é um tema excelente de portfólio. Uma API pequena, bem testada e documentada vale mais do que um projeto gigante sem acabamento. Mostre que você entende dados, HTTP, validação, erro, persistência e manutenção. É isso que transforma FastAPI de demo rápida em ferramenta de trabalho.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português