Skip to content

Santosdevbjj/api-bancaria-com-FastAPI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🏦 API Bancária Assíncrona — FastAPI + JWT + PostgreSQL

Python FastAPI PostgreSQL Docker License

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.


🧭 Índice


1. Problema de Negócio

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.


2. Contexto

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 Account vinculada — decisão de design que elimina a possibilidade de usuário sem conta.
  • Account: conta corrente com saldo (balance) inicializado em 0.0. Relacionamento 1:1 com User e 1:N com Transaction.
  • 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.


3. Premissas da Solução

  • 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 (DEPOSIT ou WITHDRAW), não pelo sinal do valor.
  • A criação de usuário é uma operação atômica: User e Account são persistidos na mesma transação com db.flush() para obter o user_id antes 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.

4. Estratégia da Solução

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

5. Technical Insights

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.


6. Resultados e Capacidades da API

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: Statement retorna current_balance + lista de transações com timestamp server-side — rastreabilidade completa por conta.
  • Documentação interativa automática: Swagger UI em /docs e ReDoc em /redoc gerados pelo FastAPI a partir dos schemas Pydantic — contrato da API sempre sincronizado com o código.

7. Como Executar o Projeto

Pré-requisitos

Com Docker Compose (recomendado)

1. Clonar o repositório

git clone https://github.com/Santosdevbjj/api-bancaria-com-FastAPI.git
cd api-bancaria-com-FastAPI

2. 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=30

3. Subir o ambiente completo

docker-compose up --build -d

O 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 dados

Execução local (sem Docker)

python -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 --reload

8. Referência de Endpoints

Fluxo completo de uso

1. 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)

Exemplos cURL

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 insuficiente

5. Extrato

curl http://localhost:8000/api/v1/transactions/statement \
  -H "Authorization: Bearer <access_token>"

9. Estrutura do Projeto

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)

10. Aprendizados

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.


11. Próximos Passos

  • Implementar SELECT ... FOR UPDATE para 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

Autor

Sérgio Santos
Senior Data Engineer & Cloud Architect | 15+ anos em sistemas críticos de Banking & Fintech

Portfólio LinkedIn GitHub


Licença

Distribuído sob a licença MIT. Veja LICENSE para mais detalhes.

About

APIs bancárias sem controle de concorrência expõem saldo a race conditions | REST assíncrona com FastAPI, JWT, bcrypt e CRUD genérico com Generic[ModelType] | Criação atômica User+Account e validação financeira antes de qualquer escrita no banco

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors