API RESTful assíncrona para operações bancárias (depósito, saque e extrato), com autenticação JWT, validações de integridade financeira e arquitetura em camadas baseada em CRUD genérico.
Desenvolvida na Formação Python Backend Developer da DIO.
- 1. Problema de Negócio
- 2. Contexto
- 3. Premissas da Solução
- 4. Estratégia da Solução
- 5. Technical Insights
- 6. Resultados e Capacidades da API
- 7. Como Executar o Projeto
- 8. Referência de Endpoints
- 9. Estrutura do Projeto
- 10. Aprendizados
- 11. Próximos Passos
- Autor
Sistemas financeiros que não enforçam regras de negócio na camada de API criam três categorias de risco:
- Risco de consistência financeira: sem validação de saldo antes do saque, a conta pode ficar negativa silenciosamente — em sistemas distribuídos, duas requisições concorrentes podem aprovar saques que juntos excedem o saldo disponível.
- Risco de autenticação: endpoints de transação sem proteção permitem que qualquer cliente autenticado opere sobre contas de terceiros, violando o princípio de autorização por recurso.
- Risco de rastreabilidade: transações sem timestamp e sem sinal de direção (positivo/negativo) impossibilitam a reconstituição do histórico financeiro — fundamental para auditoria e compliance bancário.
O objetivo deste projeto é demonstrar como esses três riscos são tratados na camada de infraestrutura da API, antes de chegarem à camada de aplicação.
Este projeto foi desenvolvido na Formação Python Backend Developer da DIO, com escopo ampliado para cobrir padrões de produção relevantes para o domínio bancário.
O domínio modela três entidades com relacionamentos bem definidos:
- User: identificado por e-mail (EmailStr), com senha armazenada como hash bcrypt. Ao ser criado, ganha automaticamente uma
Accountvinculada — decisão de design que elimina a possibilidade de usuário sem conta. - Account: conta corrente com saldo (
balance) inicializado em0.0. Relacionamento 1:1 comUsere 1:N comTransaction. - Transaction: registro imutável de cada operação. O
amounté salvo como positivo (depósito) ou negativo (saque), viabilizando o cálculo de saldo por soma direta do histórico — padrão de Event Sourcing simplificado.
A escolha de FastAPI com engine assíncrona reflete o contexto bancário: sistemas de pagamento precisam de alta concorrência para processar picos de transações sem degradação de latência.
- Todo endpoint de transação exige token JWT válido — não existe operação anônima.
- O saldo de uma conta nunca pode ser negativo: o saque é rejeitado com HTTP 400 antes de qualquer escrita no banco.
- Valores de transação devem ser estritamente positivos — a direção (crédito/débito) é determinada pelo
type(DEPOSITouWITHDRAW), não pelo sinal do valor. - A criação de usuário é uma operação atômica:
UsereAccountsão persistidos na mesma transação comdb.flush()para obter ouser_idantes do commit. - O username segue o formato e-mail (
EmailStr) como identificador único — padrão adotado por sistemas bancários digitais para reduzir atrito no cadastro.
A arquitetura segue separação de responsabilidades em camadas verticais:
Request → Router → Dependency (JWT) → Endpoint → CRUD → Model (ORM) → PostgreSQL
↓ ↓
get_current_user Validações de negócio
(decode JWT → busca User) (saldo, valor negativo, tipo)
Camadas e decisões:
| Camada | Responsabilidade | Decisão técnica |
|---|---|---|
Router (app/api/v1/) |
Agrupa endpoints por domínio | APIRouter com prefix e tags para documentação automática no Swagger |
Dependency (app/api/dependencies.py) |
Autenticação centralizada | OAuth2PasswordBearer + decode_token → injeta User autenticado via Depends |
CRUD Base (app/crud/base.py) |
Operações genéricas reutilizáveis | Generic[ModelType] — padrão que elimina código duplicado entre entidades |
| CRUD Transaction | Regras financeiras | Validação de saldo e tipo antes do db.commit(), valor negativo para saques no extrato |
| CRUD User | Criação atômica | db.flush() obtém user_id sem commit para criar Account na mesma transação |
Schema (app/schemas/) |
Contratos de entrada/saída | Pydantic v2 com condecimal, EmailStr e pattern para validação declarativa |
Model (app/db/models.py) |
Mapeamento ORM | SQLAlchemy com Mapped (type hints), relacionamentos bidirecionais e func.now() para timestamp server-side |
Security (app/core/security.py) |
JWT + bcrypt | python-jose para tokens, bcrypt para hash de senhas — bibliotecas padrão de produção Python |
Config (app/core/config.py) |
Configuração tipada | pydantic-settings com BaseSettings — variáveis de ambiente validadas no startup, falha rápida se ausentes |
Por que db.flush() antes de criar a Account?
O SQLAlchemy com AsyncSession não persiste o objeto no banco até o commit(). Para criar a Account com o user_id correto na mesma transação, é necessário emitir flush() — que envia o INSERT sem commitar, retornando o id gerado pelo PostgreSQL. Isso garante atomicidade: se a criação da Account falhar, o User também não é persistido.
Por que salvar amount como negativo no saque?
Ao invés de armazenar um campo type e um amount sempre positivo, o sinal do amount carrega a semântica da operação. Isso permite calcular o saldo atual como SUM(amount) sobre todo o histórico — equivalente a um ledger contábil. A query de extrato não precisa de CASE WHEN, o que a torna mais simples e performática.
Por que CRUDBase[ModelType] genérico?
O padrão Generic[ModelType] permite que get() e get_multi() sejam implementados uma única vez na classe base e herdados por CRUDUser, CRUDTransaction etc. Sem isso, cada entidade repetiria as mesmas queries de busca por ID — violando DRY e criando pontos de falha duplicados.
Por que asyncpg + psycopg2-binary no mesmo projeto?
asyncpg é o driver assíncrono usado pela aplicação em runtime (SQLAlchemy async). psycopg2-binary é o driver síncrono necessário para o Alembic — que executa migrations de forma síncrona. Os dois coexistem sem conflito porque operam em contextos distintos.
Por que expire_on_commit=False na SessionMaker?
Por padrão, o SQLAlchemy expira os atributos dos objetos após o commit(). Em código assíncrono, acessar esses atributos depois do commit geraria uma nova query — que no contexto async levanta MissingGreenlet. Desativar o expire_on_commit evita esse erro ao custo de manter os dados em memória até o fim do request.
A API entrega as seguintes capacidades em ambiente containerizado:
- Cadastro atômico: criar usuário automaticamente cria a conta corrente vinculada — elimina estados inconsistentes de "usuário sem conta".
- Autenticação stateless: JWT com expiração configurável via
.env— sem estado de sessão no servidor, pronto para escalonamento horizontal. - Validação financeira na camada de API: saldo insuficiente e valor negativo são rejeitados com HTTP 400 antes de qualquer escrita no banco, sem necessidade de rollback.
- Extrato com saldo calculado:
Statementretornacurrent_balance+ lista de transações com timestamp server-side — rastreabilidade completa por conta. - Documentação interativa automática: Swagger UI em
/docse ReDoc em/redocgerados pelo FastAPI a partir dos schemas Pydantic — contrato da API sempre sincronizado com o código.
- Docker 20.10+ e Docker Compose 2.0+
- Git
1. Clonar o repositório
git clone https://github.com/Santosdevbjj/api-bancaria-com-FastAPI.git
cd api-bancaria-com-FastAPI2. Configurar variáveis de ambiente
cp .env.example .env
# Edite .env com suas credenciais. Para dev local, os valores padrão já funcionam.Exemplo de .env:
# Banco de Dados
POSTGRES_USER=appuser
POSTGRES_PASSWORD=securepassword
POSTGRES_DB=bancodb
POSTGRES_HOST=db
# Segurança JWT
SECRET_KEY="SUA_CHAVE_SECRETA_MUITO_LONGA_E_ALEATORIA"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=303. Subir o ambiente completo
docker-compose up --build -dO Docker Compose irá: construir a imagem da API → subir o PostgreSQL → executar alembic upgrade head → iniciar o Uvicorn na porta 8000.
4. Acessar a documentação interativa
| Interface | URL |
|---|---|
| Swagger UI | http://localhost:8000/docs |
| ReDoc | http://localhost:8000/redoc |
| Status | http://localhost:8000/ |
5. Encerrar os serviços
docker-compose down # Para os containers
docker-compose down -v # Para e remove o volume de dadospython -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Configure .env com DATABASE_URL apontando para PostgreSQL local
alembic upgrade head
uvicorn app.main:app --reload1. POST /api/v1/users/register → cria usuário + conta
2. POST /api/v1/users/login → obtém access_token (JWT)
3. POST /api/v1/transactions/process → depósito ou saque (requer Bearer token)
4. GET /api/v1/transactions/statement → extrato + saldo (requer Bearer token)
1. Registrar usuário
curl -X POST http://localhost:8000/api/v1/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "cliente@banco.com",
"password": "senhaSegura123"
}'2. Login — obter JWT
curl -X POST http://localhost:8000/api/v1/users/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=cliente@banco.com&password=senhaSegura123"
# Resposta: {"access_token": "eyJ...", "token_type": "bearer"}3. Depósito
curl -X POST http://localhost:8000/api/v1/transactions/process \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"amount": 500.00, "type": "DEPOSIT"}'4. Saque
curl -X POST http://localhost:8000/api/v1/transactions/process \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <access_token>" \
-d '{"amount": 150.00, "type": "WITHDRAW"}'
# Retorna HTTP 400 se saldo insuficiente5. Extrato
curl http://localhost:8000/api/v1/transactions/statement \
-H "Authorization: Bearer <access_token>"api-bancaria-com-FastAPI/
├── app/
│ ├── api/
│ │ ├── dependencies.py # JWT decode → get_current_user (Depends)
│ │ └── v1/
│ │ ├── api.py # Router principal v1 — agrega users, accounts, transactions
│ │ └── endpoints/
│ │ ├── users.py # POST /register, POST /login
│ │ ├── accounts.py # Endpoints de conta
│ │ └── transactions.py # POST /process, GET /statement
│ ├── core/
│ │ ├── config.py # BaseSettings (pydantic-settings) — valida .env no startup
│ │ ├── database.py # AsyncEngine + AsyncSessionLocal + get_db()
│ │ └── security.py # bcrypt hash + JWT create/decode
│ ├── crud/
│ │ ├── base.py # CRUDBase[ModelType] genérico (get, get_multi)
│ │ ├── user.py # CRUDUser: create atômico User+Account, get_by_username
│ │ └── transaction.py # CRUDTransaction: validação financeira + persist
│ ├── db/
│ │ ├── base.py # Base declarativa com AsyncAttrs
│ │ └── models.py # User, Account, Transaction — relacionamentos ORM
│ ├── schemas/
│ │ ├── user.py # UserCreate (EmailStr), UserOut, Token, TokenData
│ │ ├── account.py # AccountOut (balance >= 0)
│ │ └── transaction.py # TransactionCreate (condecimal + pattern), TransactionOut, Statement
│ └── main.py # FastAPI init: router, title, /docs, /redoc
├── migrations/ # Alembic: versionamento de schema
├── Dockerfile # python:3.11-slim → alembic upgrade + uvicorn
├── docker-compose.yml # postgres:14-alpine + api (depends_on db)
├── alembic.ini # Configuração Alembic com DATABASE_URL
├── requirements.txt
└── .env # Variáveis de ambiente (não commitar em produção)
O que foi mais desafiador: entender o ciclo de vida da sessão assíncrona do SQLAlchemy. O comportamento de expire_on_commit=True (padrão) gera MissingGreenlet em contextos async ao acessar atributos após o commit — um erro que não aparece em código síncrono e cuja mensagem não é intuitiva. Configurar expire_on_commit=False resolve, mas exige entender a troca: os objetos em memória não são automaticamente recarregados do banco após o commit.
O que faria diferente hoje: o controle de concorrência de saldo está ausente nesta versão. Dois saques simultâneos na mesma conta podem passar ambos pela validação de saldo antes que qualquer um atualize o banco — condição de corrida clássica. A correção seria usar SELECT ... FOR UPDATE (pessimistic locking) ou UPDATE accounts SET balance = balance - :amount WHERE balance >= :amount (optimistic check na própria query).
Principal aprendizado de arquitetura: o padrão CRUDBase[ModelType] com Generic do Python parece overengineering em projetos pequenos, mas o benefício aparece quando o projeto cresce — adicionar uma nova entidade exige apenas herdar CRUDBase e implementar os métodos específicos, sem reescrever get_by_id e get_multi para cada modelo.
Sobre separação de concerns no JWT: centralizar a autenticação em get_current_user como uma dependência do FastAPI (Depends) é uma das melhores práticas do framework. O endpoint de transação não precisa saber como o token funciona — recebe diretamente o objeto User autenticado. Isso facilita trocar a estratégia de autenticação no futuro sem alterar os endpoints.
- Implementar
SELECT ... FOR UPDATEpara prevenir race condition em saques concorrentes - Adicionar paginação no extrato (
limit/offset) para contas com histórico extenso - Implementar refresh token para renovação de sessão sem re-login
- Adicionar endpoint de transferência entre contas (operação atômica com dois updates de saldo)
- Configurar rate limiting nos endpoints de autenticação para mitigar brute force
- Expandir cobertura de testes com pytest-asyncio e banco isolado por suite
Sérgio Santos
Senior Data Engineer & Cloud Architect | 15+ anos em sistemas críticos de Banking & Fintech
Distribuído sob a licença MIT. Veja LICENSE para mais detalhes.