FastAPI + React — why I chose this stack for Dogwalk
Dogwalk·

FastAPI + React — why I chose this stack for Dogwalk

Context

After defining the Dogwalk MVP, the next critical decision came: which stack to use? I knew I wanted something modern, productive, and that wouldn’t make me reinvent the wheel. But I also didn’t want to bet on tech hype that would leave me stranded 6 months down the line.

The decision process took a week — and involved more discarded prototypes than I’d like to admit.

The candidates

Before choosing, I listed the stacks I considered viable:

Stack Backend Frontend Mobile Productivity
A → FastAPI + React Python 3.11+ React + Vite PWA High
B → Next.js fullstack Node/TS Next.js PWA Medium
C → Django + HTMX Python HTML/JS PWA High
D → Flask + Alpine Python Alpine.js PWA Medium
E → Spring Boot + Angular Java/Kotlin Angular Native Low

In the early days, option B (Next.js fullstack) seemed the most attractive — one framework, TypeScript end-to-end, Vercel handling deploy. But as I dug deeper, problems emerged.

Why not Next.js?

I love JS/TS, but for this specific project, Next.js brought more questions than answers:

# Dilemma that made me jump from Next to FastAPI
# Scenario: I need to run an async task queue

# In Next.js (API routes):
# - 10s timeout on serverless functions (Vercel)
# - No native worker for background jobs
# - Solution: BullMQ + Redis + separate worker = 3 services

# In FastAPI:
# - Native BackgroundTasks
# - Celery/ARQ if you need a queue
# - WebSocket for real-time
# - Solution: 1 service + 1 optional worker

The serverless functions timeout was the biggest limiter. Dogwalk needs:

  • Photo upload with resizing
  • Optimized route calculation for dog walkers
  • Batch push notifications
  • Payment processing with reconciliation

All that in serverless with 10s timeout? It could be worked around, but with workarounds that added unnecessary complexity.

The decision: FastAPI + React

The final choice wasn’t emotional — it was a spreadsheet.

criterios = {
  "performance": {"peso": 3, "fastapi": 9, "next": 8, "django": 6},
  "ecosystem": {"peso": 3, "fastapi": 8, "next": 9, "django": 9},
  "productivity": {"peso": 4, "fastapi": 9, "next": 7, "django": 8},
  "scalability": {"peso": 2, "fastapi": 8, "next": 6, "django": 7},
  "infra_cost": {"peso": 3, "fastapi": 8, "next": 5, "django": 8},
  "maturity": {"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 won by a slim margin, but it won consistently — it was first or second in every criterion, with no serious weak point.

Why React and not something else on the frontend

With the backend decided, came the frontend choice. Here were the options:

Framework Advantages Disadvantages
React + Vite Mature ecosystem, components, PWA Large bundle, extra decisions
Vue 3 + Nuxt More opinionated, reactive Fewer available devs
Svelte Small bundle, performant New ecosystem
Alpine + SSR Simple, no build State complexity
HTMX Zero JS, pure HTML Limited in complex UI

I chose React because:

  1. Maps ecosystem — Leaflet, Mapbox, Google Maps have mature React components, essential for Dogwalk
  2. PWA with no effort — Vite + vite-plugin-pwa delivers PWA in 5 minutes
  3. TypeScript — Typing saves the day in projects with many entities (provider, owner, service, payment, review)
  4. Market — If I ever need help, React devs are easier to find

The architecture I set up

With the stack decided, I drew the project architecture:

dogwalk/
├── api/  # FastAPI backend
│  ├── app/
│  │  ├── main.py  # Entry point + middleware
│  │  ├── config.py  # Settings via pydantic-settings
│  │  ├── models/  # SQLAlchemy + Pydantic models
│  │  ├── routers/  # REST endpoints
│  │  ├── services/  # Business logic
│  │  └── workers/  # Async tasks
│  ├── alembic/  # Migrations
│  └── tests/  # Tests with pytest

├── web/  # React frontend
│  ├── src/
│  │  ├── components/  # Reusable components
│  │  ├── pages/  # Application pages
│  │  ├── hooks/  # Custom hooks
│  │  ├── services/  # API client (axios)
│  │  └── stores/  # Global state (zustand)
│  ├── public/  # Static assets
│  └── tests/  # Tests with vitest

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

One decision I made a point of making early: clear separation between api and web, in different folders, each with its own package.json/pyproject.toml. No monorepo with everything mixed together. Each part can be developed, tested, and deployed independently.

Environment setup: what worked

After some trial and error, the ideal setup looked like this:

# 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

The secret I discovered: pydantic-settings with .env file. This saved countless config headaches:

# 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',
  },
});

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

The first endpoints

With the setup working, I wrote the first real endpoints:

# 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),
):
  """List providers with optional geolocation filter"""
  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="Provider not found")
  return prestador

FastAPI’s async really shone here — with asyncpg and AsyncSession, queries run without blocking the event loop, and the API can handle hundreds of concurrent requests even on a small instance.

What I learned

1. Unified stack (JS/TS fullstack) looks attractive but comes at a price

Next.js is beautiful for landing pages and blogs. For an app with background jobs, WebSocket, and server-side processing, you end up setting up the same infra you’d have with FastAPI — just in JS.

2. Front/backend separation isn’t dogma — it’s pragmatism

Having the backend in Python and the frontend in React means each side uses the right libraries for its domain. Python has SQLAlchemy, Alembic, Pydantic, Celery/ARQ for the backend. React has UI ecosystem, maps, state. No one has to make concessions.

3. Pydantic v2 is one of the best Python packages around

Validation, serialization, automatic documentation (OpenAPI), settings management, type hints — Pydantic v2 does all this with native performance (Rust). It’s the kind of library that improves every piece of code it touches.

4. Config via .env + pydantic-settings saves lives

In the beginning, I had configs scattered as constants in the code. On one of the first deploys, a hardcoded DATABASE_URL leaked into the repository. Since I migrated to .env, I’ve never had this problem again.

5. TypeScript on the frontend + Pydantic on the backend = consistency

I create the Pydantic schemas on the backend and the corresponding TypeScript types on the frontend. It’s not automatic (one day I’ll build a generator), but the discipline of keeping both in sync has already prevented at least 5 type bugs in API calls.

The stack numbers

Aspect Result
Frameworks considered 5
Days of decision 7
Endpoints created in first sprint 12
React components in first sprint 8
Tests passing 47
ms per request (average) 12
Lines of configuration ~300

TL;DR: I chose FastAPI + React after a week of analysis comparing 5 stacks against objective criteria. FastAPI won for the combination of performance, productivity, and Python ecosystem. React won for the maturity of maps and PWA ecosystem. The clear separation between frontend and backend lets each side use the right tools without compromise.


Useful commands

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