---
title: "SQLModel com FastAPI: APIs Tipadas com Banco de Dados"
url: "https://python.dev.br/blog/sqlmodel-fastapi-python/"
markdown_url: "https://python.dev.br/blog/sqlmodel-fastapi-python.MD"
description: "Aprenda a usar SQLModel com FastAPI para criar APIs Python tipadas, com modelos reutilizáveis, SQLite, sessões, CRUD, testes e boas práticas."
date: "2026-05-31"
author: "Equipe Python Brasil"
---

# 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](/glossario/fastapi/), [Pydantic](/glossario/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](/guias/criando-api-fastapi/), [SQLAlchemy 2.0](/blog/sqlalchemy-2-orm-moderno-python/), [testes com pytest](/guias/testes-com-pytest/) e [Docker em projetos Python](/guias/configurando-docker-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:

```bash
python -m venv .venv
source .venv/bin/activate

pip install fastapi uvicorn sqlmodel pytest httpx
```

Com `uv`, o fluxo fica assim:

```bash
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:

```text
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`:

```python
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`:

```python
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`:

```python
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:

```bash
uvicorn app.main:app --reload
```

Teste com `curl`:

```bash
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`:

```python
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`:

```python
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`:

```python
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](/guias/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](/blog/opentelemetry-python-observabilidade/) 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 <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go para concorrência e binários simples</a>. 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](/vagas/), [projetos de portfólio Python](/carreira/projetos-portfolio-python/) e [teste técnico Python](/carreira/teste-tecnico-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.
