Primeiros passos — infraestrutura e as primeiras linhas de código
🐶 Dogwalk·

Primeiros passos — infraestrutura e as primeiras linhas de código

📖 10 min de leitura← Voltar para timeline

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://[email protected]: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

  1. 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 para 127.0.0.1 e adicionei --no-server-header para não vazar versão.

  2. Timezone naive vs aware — PostgreSQL rejeita comparar timestamp without time zone com datetime.now(timezone.utc). Solução: datetime.now(timezone.utc).replace(tzinfo=None).

  3. MemoryMax no WSL — Coloquei MemoryMax=2G no systemd e o backend reiniciava a cada 3 minutos. WSL2 não suporta cgroup v2 direito. Removi a diretiva e tudo ficou estável.

  4. 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.


Comandos úteis

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