FastAPI + React — por que escolhi essa stack pro Dogwalk
🐶 Dogwalk·

FastAPI + React — por que escolhi essa stack pro Dogwalk

📖 12 min de leitura← Voltar para timeline

Contexto

Depois de definir o MVP do Dogwalk, veio a próxima decisão crítica: qual stack usar? Eu sabia que queria algo moderno, produtivo e que não me fizesse reinventar a roda. Mas também não queria apostar em hype tecnológico que ia me deixar na mão daqui 6 meses.

O processo de decisão levou uma semana — e envolveu mais protótipos descartados do que gostaria de admitir.

As candidatas

Antes de escolher, listei as stacks que considerei viáveis:

Stack Backend Frontend Mobile Produtividade
A → FastAPI + React Python 3.11+ React + Vite PWA Alta
B → Next.js fullstack Node/TS Next.js PWA Média
C → Django + HTMX Python HTML/JS PWA Alta
D → Flask + Alpine Python Alpine.js PWA Média
E → Spring Boot + Angular Java/Kotlin Angular Native Baixa

Nos primeiros dias, a opção B (Next.js fullstack) parecia a mais atraente — um framework só, TypeScript de ponta a ponta, Vercel cuidando do deploy. Mas conforme fui aprofundando, problemas apareceram.

Por que não Next.js?

Eu amo JS/TS, mas para esse projeto específico, Next.js trouxe mais dúvidas que respostas:

# Dilema que me fez pular do Next pra FastAPI
# Cenário: preciso rodar uma fila de tarefas assíncronas

# No Next.js (API routes):
# - Timeout de 10s em serverless functions (Vercel)
# - Sem worker nativo pra background jobs
# - Solução: BullMQ + Redis + worker separado = 3 serviços

# No FastAPI:
# - BackgroundTasks nativo
# - Celery/ARQ se precisar de fila
# - WebSocket pra tempo real
# - Solução: 1 serviço + 1 worker opcional

O timeout de serverless functions foi o maior limitador. O Dogwalk precisa de:

  • Upload de fotos com redimensionamento
  • Cálculo de rotas otimizadas pra dog walker
  • Notificações push em lote
  • Processamento de pagamentos com conciliação

Tudo isso em serverless de 10s de timeout? Dava pra contornar, mas com workarounds que adicionavam complexidade desnecessária.

A decisão: FastAPI + React

A escolha final não foi emocional — foi uma planilha.

criterios = {
    "performance": {"peso": 3, "fastapi": 9, "next": 8, "django": 6},
    "ecossistema": {"peso": 3, "fastapi": 8, "next": 9, "django": 9},
    "produtividade": {"peso": 4, "fastapi": 9, "next": 7, "django": 8},
    "escalabilidade": {"peso": 2, "fastapi": 8, "next": 6, "django": 7},
    "custo_infra": {"peso": 3, "fastapi": 8, "next": 5, "django": 8},
    "maturidade": {"peso": 2, "fastapi": 7, "next": 7, "django": 10},
}

def calcular_nota(criterios, stack):
    total = sum(
        v["peso"] * v[stack]
        for k, v in criterios.items()
    )
    return total / sum(v["peso"] for v in criterios.values())

for stack in ["fastapi", "next", "django"]:
    print(f"{stack}: {calcular_nota(criterios, stack):.2f}")

# → fastapi: 8.35
# → next: 7.06
# → django: 7.82

FastAPI venceu por pouco, mas venceu com consistência — ficou em primeiro ou segundo em todos os critérios, sem nenhum ponto fraco grave.

Por que React e não outra coisa no front

Com o backend decidido, veio a escolha do frontend. Aqui as opções eram:

Framework Vantagens Desvantagens
React + Vite Ecossistema maduro, componentes, PWA Bundle grande, decisões extras
Vue 3 + Nuxt Mais opinionado, reativo Menos devs disponíveis
Svelte Bundle pequeno, performático Ecossistema novo
Alpine + SSR Simples, sem build Complexidade no estado
HTMX Zero JS, HTML puro Limitações em UI complexa

Escolhi React porque:

  1. Ecossistema de mapas — Leaflet, Mapbox, Google Maps têm React components maduros, essenciais pro Dogwalk
  2. PWA sem esforço — Vite + vite-plugin-pwa entrega PWA em 5 minutos
  3. TypeScript — Tipagem salva em projeto com muitas entidades (prestador, dono, serviço, pagamento, avaliação)
  4. Mercado — Se um dia eu precisar de ajuda, React devs são mais fáceis de encontrar

A arquitetura que montei

Com a stack decidida, desenhei a arquitetura do projeto:

dogwalk/
├── api/                       # FastAPI backend
│   ├── app/
│   │   ├── main.py            # Entry point + middleware
│   │   ├── config.py          # Settings via pydantic-settings
│   │   ├── models/            # SQLAlchemy + Pydantic models
│   │   ├── routers/           # Endpoints REST
│   │   ├── services/          # Lógica de negócio
│   │   └── workers/           # Tarefas assíncronas
│   ├── alembic/               # Migrations
│   └── tests/                 # Testes com pytest

├── web/                       # React frontend
│   ├── src/
│   │   ├── components/        # Componentes reutilizáveis
│   │   ├── pages/             # Páginas da aplicação
│   │   ├── hooks/             # Custom hooks
│   │   ├── services/          # API client (axios)
│   │   └── stores/            # Estado global (zustand)
│   ├── public/                # Assets estáticos
│   └── tests/                 # Testes com vitest

└── infra/                     # Docker + deploy
    ├── docker-compose.yml
    ├── Dockerfile.api
    ├── Dockerfile.web
    └── nginx.conf

Uma decisão que fiz questão de tomar cedo: separação clara entre api e web, em pastas diferentes, cada uma com seu package.json/pyproject.toml. Nada de monorepo com tudo misturado. Cada parte pode ser desenvolvida, testada e deployada independentemente.

Setup do ambiente: o que funcionou

Depois de algumas tentativas e erros, o setup ideal ficou assim:

# Backend
cd api
python3 -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn sqlalchemy asyncpg pydantic-settings

# Frontend
cd web
npm create vite@latest . -- --template react-ts
npm install react-router-dom zustand axios leaflet
npm install -D @types/leaflet tailwindcss postcss autoprefixer

O segredo que descobri: pydantic-settings com .env file. Isso salvou incontáveis headaches de config:

# api/app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    app_name: str = "Dogwalk API"
    debug: bool = False
    database_url: str = "postgresql+asyncpg://localhost:5432/dogwalk"
    secret_key: str = "change-me-in-production"
    cors_origins: list[str] = ["http://localhost:5173"]
    sentry_dsn: str | None = None
    cloudflare_r2_endpoint: str | None = None
    maps_api_key: str | None = None

    model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}

@lru_cache()
def get_settings():
    return Settings()
// web/src/services/api.ts
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor pra token JWT
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

Os primeiros endpoints

Com o setup funcionando, escrevi os primeiros endpoints reais:

# api/app/routers/prestadores.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import Prestador
from app.schemas import PrestadorOut, PrestadorCreate

router = APIRouter(prefix="/prestadores", tags=["prestadores"])

@router.get("/", response_model=list[PrestadorOut])
async def listar_prestadores(
    lat: float | None = None,
    lng: float | None = None,
    raio_km: float = 5.0,
    servico: str | None = None,
    db: AsyncSession = Depends(get_db),
):
    """Lista prestadores com filtro opcional por geolocalização"""
    query = "SELECT * FROM prestadores WHERE 1=1"
    params = {}

    if lat and lng:
        query += """ AND ST_DWithin(
            ST_MakePoint(:lng, :lat)::geography,
            localizacao::geography,
            :raio
        )"""
        params["lat"] = lat
        params["lng"] = lng
        params["raio"] = raio_km * 1000

    if servico:
        query += " AND :servico = ANY(servicos)"
        params["servico"] = servico

    result = await db.execute(query, params)
    return result.scalars().all()


@router.get("/{prestador_id}", response_model=PrestadorOut)
async def detalhe_prestador(
    prestador_id: int,
    db: AsyncSession = Depends(get_db),
):
    prestador = await db.get(Prestador, prestador_id)
    if not prestador:
        raise HTTPException(status_code=404, detail="Prestador não encontrado")
    return prestador

O async do FastAPI brilhou aqui — com asyncpg e AsyncSession, as queries rodam sem bloquear o event loop, e a API consegue lidar com centenas de requisições concorrentes mesmo numa instância pequena.

O que aprendi

1. Stack unificada (JS/TS fullstack) parece atraente mas cobra um preço

Next.js é lindo pra landing pages e blogs. Pra um app com background jobs, WebSocket, e processamento server-side, você acaba montando a mesma infra que teria com FastAPI — só que em JS.

2. A separação front/backend não é dogma — é pragmatismo

Ter o backend em Python e o frontend em React significa que cada lado usa as bibliotecas certas pro seu domínio. Python tem SQLAlchemy, Alembic, Pydantic, Celery/ARQ pra backend. React tem ecossistema de UI, mapas, estado. Ninguém precisa fazer concessões.

3. Pydantic v2 é um dos melhores pacotes Python da atualidade

Validação, serialização, documentação automática (OpenAPI), settings management, type hints — o Pydantic v2 faz tudo isso com desempenho nativo (Rust). É o tipo de biblioteca que melhora cada parte do código que toca.

4. Config via .env + pydantic-settings salva vidas

No começo, eu tinha as configs espalhadas em constantes no código. Num dos primeiros deploys, um DATABASE_URL hardcoded vazou pro repositório. Desde que migrei pra .env, nunca mais tive esse problema.

5. TypeScript no frontend + Pydantic no backend = consistência

Eu crio os schemas Pydantic no backend e os tipos TypeScript correspondentes no frontend. Não é automático (um dia faço um gerador), mas a disciplina de manter os dois sincronizados já evitou pelo menos 5 bugs de tipo em chamadas de API.

Os números da stack

Aspecto Resultado
Frameworks considerados 5
Dias de decisão 7
Endpoints criados no primeiro sprint 12
Componentes React no primeiro sprint 8
Testes passando 47
ms por requisição (média) 12
Linhas de configuração ~300

TL;DR: Escolhi FastAPI + React depois de uma semana de análise comparando 5 stacks contra critérios objetivos. FastAPI venceu pela combinação de performance, produtividade e ecossistema Python. React pela maturidade do ecossistema de mapas e PWA. A separação clara entre front e backend permite que cada lado use as ferramentas certas sem concessão.


Comandos úteis

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$