
Primeiros passos — infraestrutura e as primeiras linhas de código
Ter a ideia foi fácil. Colocar ela de pé foi outra história.
Quatro dias depois do estalo inicial, eu estava pronto para começar a codar de verdade. Mas antes precisava responder uma pergunta crucial: qual stack usar?
A escolha da stack
React 19 tinha acabado de sair, Vite 8 prometia build imediato, e FastAPI era a escolha óbvia para o backend — performance boa, tipagem nativa com Pydantic, e documentação automática.
# O comando que deu início a tudo
npm create vite@latest dogwalk -- --template react-ts
cd dogwalk
npm install
Parece simples agora, mas na época enfrentei uns bons perrengues. Configuração de ESLint com React 19 deu briga, o Vite 8 tinha acabado de sair com algumas breaking changes, e o Stripe me fez perder uma tarde inteira porque esqueci de configurar o webhook no ambiente de desenvolvimento.
Comparação de tecnologias
Antes de bater o martelo, fiz uma planilha comparativa:
| Critério | React 19 | Vue 3.5 | Next.js | Svelte |
|---|---|---|---|---|
| Build tool | Vite 8 ⚡ | Vite 8 ⚡ | Webpack 🐢 | Vite 8 ⚡ |
| SSR/SSG | ❌ SPA puro | ❌ SPA puro | ✅ SSR nativo | ❌ SPA puro |
| TypeScript | ✅ Nativo | ✅ Nativo | ✅ Nativo | ✅ Nativo |
| Curva aprendizado | 🟢 Conheço bem | 🟡 Razoável | 🟢 Conheço bem | 🟠 Aprenderia |
| Ecossistema maturidade | 🟢 Gigante | 🟢 Grande | 🟢 Grande | 🟡 Crescendo |
| Bundle size (base) | ~40 KB | ~33 KB | ~70 KB | ~5 KB |
| Veredito | ✅ Escolhido | ⬅️ 2ª opção | ❌ Overkill p/ SPA | ❌ Risco |
Backend:
| Critério | FastAPI | Flask | Express | Django |
|---|---|---|---|---|
| Performance | 🟢 Assíncrono | 🟡 Síncrono | 🟢 Assíncrono | 🟡 Síncrono |
| Tipagem | ✅ Pydantic | ❌ | ❌ (JS) | ✅ (médio) |
| Docs automáticas | ✅ Swagger | ❌ | ❌ | ❌ |
| Async DB | ✅ asyncpg | 🟡 gevent | ✅ Prisma | 🟡 |
| ORM | ✅ SQLAlchemy | ✅ SQLAlchemy | ✅ Prisma | ✅ ORM próprio |
| Peso | 🟢 Leve | 🟢 Leve | 🟢 Leve | 🔴 Pesado |
| Veredito | ✅ Escolhido | ❌ Sem async | ❌ Sem tipagem | ❌ Pesado demais |
A primeira rota: cadastro de usuário
Com a stack definida, escrevi as primeiras rotas de verdade. O cadastro de usuário foi o primeiro endpoint que conectou frontend e backend:
# backend/app/routers/auth.py — primeira rota funcional
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, EmailStr
import bcrypt
from app.database import get_db
router = APIRouter(prefix="/auth", tags=["auth"])
class RegisterRequest(BaseModel):
name: str
email: EmailStr
password: str
role: str # 'tutor' | 'passeador'
class UserResponse(BaseModel):
id: str
name: str
email: str
role: str
created_at: str
@router.post("/register", response_model=UserResponse)
async def register(req: RegisterRequest):
if req.role not in ("tutor", "passeador"):
raise HTTPException(400, "Role inválida")
hashed = bcrypt.hashpw(
req.password.encode(), bcrypt.gensalt()
).decode()
async with get_db() as db:
# Verifica se email já existe
existing = await db.fetchrow(
"SELECT id FROM users WHERE email = $1", req.email
)
if existing:
raise HTTPException(409, "Email já cadastrado")
# Cria usuário + profile em transação
user = await db.fetchrow(
"""INSERT INTO users (name, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, name, email, created_at""",
req.name, req.email, hashed
)
await db.execute(
"""INSERT INTO profiles (user_id, name, role)
VALUES ($1, $2, $3)""",
user["id"], req.name, req.role
)
return {
"id": str(user["id"]),
"name": user["name"],
"email": user["email"],
"role": req.role,
"created_at": user["created_at"].isoformat(),
}
O uso de asyncpg diretamente (sem ORM) foi intencional — eu queria sentir o banco antes de abstrair. Só depois adicionei SQLAlchemy para consultas mais complexas.
Docker e PostgreSQL
O banco rodava em Docker desde o início. A primeira docker-compose.yml era simples:
# docker-compose.yml — versão 0.1
version: "3.9"
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_DB: dogwalk
POSTGRES_USER: dogwalk
# opcional: acesso configurado via env externo
POSTGRES_ACCESS: local_dev
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./backend/migrations:/docker-entrypoint-initdb.d
volumes:
pgdata:
O volume de migrations montado em docker-entrypoint-initdb.d fazia o PostgreSQL executar os SQLs de schema na primeira inicialização. Era tosco, mas funcionava.
A primeira migration (que sobreviveu)
-- backend/migrations/001_users_profiles.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('tutor', 'passeador')),
phone TEXT,
avatar_url TEXT,
city TEXT,
rating DECIMAL(2,1) DEFAULT 0.0,
is_verified BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_profiles_user_id ON profiles(user_id);
CREATE INDEX idx_profiles_city ON profiles(city);
CREATE INDEX idx_profiles_role ON profiles(role);
Essa migration é a base de tudo até hoje. Claro, depois vieram mais 15 tabelas (pets, bookings, walks, gps_points, reviews, chat_messages, notifications, financial_transactions, etc.), mas a estrutura de users + profiles se manteve.
As primeiras lutas
Olhando hoje o deploy-workflow.sh, parece um script de boa. Mas ele nasceu de um sábado inteiro tentando entender por que o build quebrava em produção mas não em dev.
#!/bin/bash
# deploy-workflow.sh — versão 0.1, 28/05/2026
echo "🐶 Buildando Dogwalk..."
npm run build && echo "✅ Build OK" || echo "❌ Build falhou"
Sim, era literalmente isso no começo. Cresceu com condicionais, health checks, logs, e hoje tem umas 80 linhas. Mas todo script grande começa pequeno.
Configuração de ambiente
O gerenciamento de variáveis de ambiente foi outro aprendizado. O primeiro .env era mínimo:
# .env — primeira versão
DOGWALK_JWT_CONFIG=dev-mode
DOGWALK_DB_URL=postgresql+asyncpg://dogwalk@127.0.0.1:5432/dogwalk
VITE_API_URL=http://localhost:8080
# chaves de pagamento configuradas via env
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_exemplo
Depois veio o EnvironmentFile= no systemd, a separação .env.development / .env.production, e o openssl rand -hex 32 para gerar secrets de verdade. Mas no começo, era só isso.
PostgreSQL e a primeira migration
A escolha do banco foi PostgreSQL rodando em Docker. Nada de ORM complexo no começo — SQL puro mesmo, com asyncpg no FastAPI.
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT CHECK (role IN ('tutor', 'passeador')) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
Essa foi literalmente a primeira tabela. Ela existe até hoje, com algumas colunas a mais e um punhado de índices.
O que aprendi
Que setup de projeto é traiçoeiro — parece que você tá progredindo quando na verdade só tá configurando ferramentas. Mas sem essa base, o resto desmorona. Cada hora gasta na infra no começo economizou dias de dor de cabeça depois.
Pitfalls que marcaram
-
bind 0.0.0.0 vs 127.0.0.1 — No começo o uvicorn ouvia em
0.0.0.0, expondo o backend na rede local. Corrigi para127.0.0.1e adicionei--no-server-headerpara não vazar versão. -
Timezone naive vs aware — PostgreSQL rejeita comparar
timestamp without time zonecomdatetime.now(timezone.utc). Solução:datetime.now(timezone.utc).replace(tzinfo=None). -
MemoryMax no WSL — Coloquei
MemoryMax=2Gno systemd e o backend reiniciava a cada 3 minutos. WSL2 não suporta cgroup v2 direito. Removi a diretiva e tudo ficou estável. -
Rollup/Rolldown — Vite 8 usa Rolldown (Rust). Ele não aceita imports no final do arquivo, e closures inline com block body em JSX quebram. Precisei extrair subcomponentes em vários lugares.