O Contexto
A NBA é a liga esportiva mais seguida pelos brasileiros fora do futebol, mas as plataformas existentes são majoritariamente em inglês e não contextualizam o esporte para o público local. O NaQuadra surgiu como resposta direta a essa lacuna: uma plataforma 100% em português, com dados em tempo real e foco na experiência educativa do fã casual.
O projeto nasceu como um desafio técnico pessoal para demonstrar domínio de arquitetura full-stack moderna — monorepo com separação clara entre frontend (Next.js) e backend (NestJS), consumindo a API pública da NBA com cache inteligente para evitar rate limiting.
Arquitetura & Stack
Frontend
- • Next.js 14 (App Router)
- • React Server Components
- • TypeScript strict
- • Tailwind CSS
- • Lucide Icons
Backend
- • NestJS
- • Módulos injetáveis
- • Guards & Interceptors
- • HTTP Cache (TTL)
- • NBA API client
Monorepo
- • Turborepo
- • packages/shared
- • Types unificados
- • Build pipeline
- • .env por workspace
DevOps
- • Vercel (web)
- • vercel.json (root dir)
- • GitHub Actions CI
- • Preview deployments
- • Env management
Por que Monorepo?
A decisão de usar um monorepo foi arquitetural deliberada. Com frontend e backend no mesmo repositório, compartilhamos tipos TypeScript entre as camadas — o mesmo GameDTO que o NestJS retorna é o mesmo que o Next.js consome. Isso elimina erros de integração e mantém o contrato de API sempre atualizado.
Cache na Borda
A NBA API tem rate limiting rigoroso. Implementei um cache TTL no NestJS e configurei revalidate no Next.js para dados históricos. Dados de placar ao vivo têm TTL curto (30s); standings têm TTL de 1h. Essa estratégia reduziu as requisições à API externa em ~85%.
// Cache com TTL dinâmico baseado no tipo de dado
@Injectable()
export class GamesService {
constructor(
private readonly httpService: HttpService,
private readonly cacheManager: Cache,
) {}
async getLiveScores(): Promise<GameDTO[]> {
const cacheKey = 'live_scores';
const cached = await this.cacheManager.get<GameDTO[]>(cacheKey);
if (cached) return cached; // Hit — sem API call
const data = await this.fetchFromNBAApi('/games/today');
await this.cacheManager.set(cacheKey, data, 30_000); // TTL 30s
return data;
}
}
O Maior Desafio
Configurar o Vercel para reconhecer apenas o apps/web em um repositório monorepo sem acionar builds do backend foi o maior ponto de atrito. O Vercel por padrão tenta buildar a raiz do projeto — foi necessário criar um vercel.json apontando para o subdiretório correto e configurar variáveis de ambiente isoladas por workspace.
A Solução
Criei um vercel.json dentro de apps/web que instrui o Vercel a usar aquele diretório como root. Combinado com Turborepo para gerenciar o pipeline de build do monorepo, o deploy passou a ser determinístico e isolado — web e API deployam independentemente sem interferência.
Aprendizados
Monorepos com Turborepo exigem convenções claras desde o início — naming de packages, scripts unificados e .env isolados por app.
React Server Components mudaram radicalmente como penso data fetching — buscar no servidor e entregar HTML pronto elimina waterfalls de cliente.
Configurar deploy de monorepos em PaaS requer documentação específica. Valeu criar um ADR (Architecture Decision Record) para cada decisão de infraestrutura.