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.

8 min de leitura Equipe Python Brasil

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árioPor que SQLModel ajuda
API CRUD com FastAPImodelos de entrada, saída e tabela ficam próximos
Projeto de portfóliomenos boilerplate e mais foco em regra de negócio
Sistema internovalidação e persistência ficam fáceis de explicar
Teste técnicocódigo compacto, legível e tipado
MVP com SQLite/PostgreSQLcomeç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çãoMelhor quando
SQLModelFastAPI, CRUD tipado, MVP, portfólio, APIs pequenas e médias
SQLAlchemy 2.0domínio complexo, controle fino, queries avançadas, múltiplos padrões
Django ORMprojeto 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.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português