Ir para o conteúdo
Carreira

O Mapa Completo do Dev Web Moderno: Ferramentas, Decisões e Anti-Patterns que Separam Júnior de Sênior

Marcos Soares
Atualizado em 
29 minutos de leitura
Ilustracao 3D de uma rosa dos ventos de vidro fosco sobre mapa topografico representando guia completo do dev web
Ouça este artigo
0:00O Mapa Completo do Dev Web Moderno: Ferramentas, Decisões e Anti-Patterns que Separam Júnior de Sênior--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

Resumo rápido

Este artigo é um sistema de decisão para montar sua stack web em 2026. Você sai daqui com: um framework de 5 passos para escolher qualquer tecnologia, tabelas comparativas com dados de downloads e benchmarks reais, anti-patterns com código errado vs correto, e um roadmap progressivo de 5 níveis de maturidade. Não é um catálogo de ferramentas. É um guia de engenharia que eu uso na Agência Poti para onboarding de devs plenos e seniors.

O problema: por que 90% dos mapas de stack falham

Em 2019, montei um "mapa de tecnologias" para o time de um cliente no setor de logística. Era bonito: caixas coloridas, setas conectando HTML até Kubernetes, um roadmap linear de 18 meses. Três meses depois, dois devs saíram frustrados porque o mapa dizia o que aprender, mas não por que escolher uma coisa e não outra.

O problema de todo "mapa completo" é que ele vira catálogo. Lista 47 tecnologias, coloca um check ao lado de cada uma, e o leitor fecha a aba sem saber responder a pergunta que importa: qual stack eu monto para o meu cenário específico?

Eu cometi esse erro no primeiro artigo que escrevi sobre esse tema. Listei frameworks, build tools, bibliotecas de estado. Tudo correto, tudo raso. Desde então, operei com mais de 30 projetos em produção na Agência Poti e em consultorias, e o que aprendi é direto: dev sênior não precisa de lista. Precisa de critério de decisão.

Este artigo substitui catálogo por sistema. Cada seção tem valor independente. Você lê só a parte de testes e sai sabendo migrar de Jest para Vitest. Lê só a parte de deploy e sai com uma decision tree para escolher entre Vercel, AWS e VPS. Essa é a proposta.

Framework de decisão: como escolher qualquer stack em 5 passos

Eu uso esses 5 passos em toda consultoria de arquitetura. Funcionam para escolher framework, ORM, cloud provider, qualquer coisa.

Passo 1: Defina o constraint primário

Todo projeto tem um gargalo dominante. Identifique qual é o seu:

  • Tempo: MVP em 3 meses, precisa de velocidade de desenvolvimento
  • Escala de time: 15+ devs, precisa de conventions fortes e tipagem rígida
  • Performance: dashboard real-time, precisa de bundle mínimo e rendering otimizado
  • Custo: freelancer solo, precisa de deploy barato e ecossistema maduro
  • Contratação: mercado BR, precisa de pool de candidatos disponíveis

Em 2023, um cliente de fintech me pediu para escolher entre Svelte e React. O constraint primário era contratação: eles precisavam de 8 devs em São Paulo em 60 dias. Svelte tem 3.3M de downloads semanais. React tem 96M. A resposta se deu sozinha.

Passo 2: Elimine por deal-breakers

Não compare 5 opções. Elimine as que não passam no filtro mínimo:

TYPESCRIPT
// Decision filter - aplique antes de comparar qualquer coisa
interface StackFilter {
  hasTypescriptSupport: boolean;    // Eliminatório em 2026
  weeklyDownloads: number;          // < 1M = risco de abandono
  lastMajorRelease: Date;           // > 18 meses = red flag
  brJobPostings: number;            // GeekHunter + Gupy, últimos 90 dias
  teamExperience: 'none' | 'some' | 'strong';
}
 
function shouldEliminate(filter: StackFilter): boolean {
  if (!filter.hasTypescriptSupport) return true;
  if (filter.weeklyDownloads < 500_000) return true;
  if (daysSince(filter.lastMajorRelease) > 540) return true;
  if (filter.brJobPostings < 50 && filter.teamExperience === 'none') return true;
  return false;
}

Passo 3: Pese ecossistema sobre features

Feature individual é irrelevante se o ecossistema não sustenta. Eu avalio ecossistema por 3 métricas: número de pacotes compatíveis no npm, qualidade da documentação oficial, e frequência de respostas no Stack Overflow/GitHub Discussions nos últimos 6 meses.

Em 2021, escolhi Solid.js para um projeto pessoal. Performance absurda. Mas quando precisei de uma lib de formulários com validação server-side, gastei 3 dias adaptando algo que no React levaria 20 minutos com React Hook Form.

Passo 4: Prototipe em 4 horas, não em 4 semanas

Monte o fluxo mais crítico do seu app nas 2-3 opções finalistas. Meça: tempo de setup, clareza do código, e se a documentação respondeu suas dúvidas sem precisar de Stack Overflow.

Bash
# Cronômetro real: monte um CRUD com auth em cada framework
# Meça tempo total do zero ao deploy
 
# Next.js (App Router)
npx create-next-app@latest --typescript --tailwind --app
# Meu tempo médio: 3h42min para CRUD + auth + deploy na Vercel
 
# SvelteKit
npx sv create my-app
# Meu tempo médio: 4h15min (menos familiaridade, docs excelentes)
 
# Nuxt 3
npx nuxi@latest init my-app
# Meu tempo médio: 3h58min (Vue familiar, ecossistema menor)

Passo 5: Valide com o cenário de 18 meses

Pergunte: "Se eu precisar dobrar o time em 18 meses, consigo contratar? Se o framework lançar breaking changes, a migração é viável? Se o maintainer principal sair, o projeto sobrevive?"

Rich Harris, criador do Svelte, foi contratado pela Vercel. Evan You, criador do Vue e Vite, mantém ambos com funding independente. Dan Abramov saiu do time core do React. Essas movimentações importam mais do que benchmarks de bundle size.

Frameworks em 2026: dados, não opinião

O State of JS 2025 trouxe um dado que deveria ter encerrado a conversa sobre "framework fatigue": a média de frameworks que um dev usa na carreira inteira é 2.6. Não 2.6 por ano. Por carreira.

Isso significa que a escolha do framework é uma das decisões de maior impacto na sua trajetória. Vamos olhar os números.

Tabela comparativa: React vs Vue vs Svelte

MétricaReactVueSvelte
Downloads/semana (npm)96M9M3.3M
Bundle min+gzip44.5 KB33 KB1.8 KB*
ArquiteturaVDOM, runtimeVDOM + reatividadeCompile-time, sem VDOM
TypeScriptNativo (JSX)Nativo (Vue 3+)Nativo (Svelte 5+)
Meta-frameworkNext.js (6.5M/sem)Nuxt (1.2M/sem)SvelteKit (450K/sem)
Satisfação (State of JS 2025)69%72%88%
Vagas BR (GeekHunter, Q1 2026)~4.200~680~120

O asterisco no Svelte é crucial. O bundle de 1.8 KB é o runtime base. Cada componente Svelte adiciona código compilado. Em apps com 19+ componentes do tamanho de um TodoMVC, o bundle total do Svelte ultrapassa o do React. Esse dado vem de benchmarks reproduzíveis do framework-benchmarks do krausest no GitHub. Svelte é mais leve para landing pages e apps pequenos. Para dashboards com 50+ componentes, a vantagem desaparece.

O moat do Angular em bancos brasileiros

Angular tem um fenômeno único no Brasil que não existe em nenhum outro mercado. Itaú, Bradesco, TOTVS e Banco do Brasil operam frontends críticos em Angular. O motivo não é performance. É que Angular impõe conventions: módulos, injeção de dependência, estrutura de pastas, tudo padronizado. Com times de 40+ devs, isso reduz o caos.

Eu consultei para uma fintech que integrava com APIs do Itaú em 2022. O requisito contratual era Angular. Sem negociação. Se você mira enterprise financeiro no Brasil, Angular não é opção. É pré-requisito.

Astro: a métrica que ninguém reporta

O Astro tem 39 pontos de satisfação acima do Next.js no State of JS 2025. Essa é a métrica mais sub-reportada do ecossistema. Para projetos content-first (blogs, docs, landing pages, sites de marketing), Astro entrega zero JavaScript por default e permite "islands" de React, Vue ou Svelte onde interatividade é necessária.

Eu migrei o Vivo de Código para uma arquitetura similar e o LCP caiu de 2.1s para 890ms. Zero JS no client para páginas de artigo. Islands de React só nos componentes interativos.

O shift arquitetural: server state vs client state

A maior mudança arquitetural no frontend dos últimos 3 anos não foi um framework novo. Foi a separação entre server state (dados que vêm de APIs) e client state (UI state local: modais abertos, filtros selecionados, tema dark/light).

Redux misturava tudo num store gigante. Em 2026, isso é um anti-pattern. A combinação TanStack Query + Zustand resolve 95% dos casos com menos código e melhor performance.

Server state com TanStack Query

TanStack Query (12M downloads/semana) cuida de cache, refetch, stale-while-revalidate, e sincronização com o servidor. Tudo que Redux Toolkit Query tentou fazer, mas com API mais limpa.

TYPESCRIPT
// Server state: dados que vêm da API
// TanStack Query gerencia cache, loading, error, refetch
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 
interface User {
  id: string;
  name: string;
  email: string;
}
 
// Buscar lista de usuários com cache automático de 5 minutos
function useUsers() {
  return useQuery<User[]>({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users');
      if (!res.ok) throw new Error('Falha ao buscar usuários');
      return res.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutos antes de considerar stale
    gcTime: 10 * 60 * 1000,   // 10 minutos no garbage collection
  });
}
 
// Mutation com invalidação automática do cache
function useCreateUser() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: async (newUser: Omit<User, 'id'>) => {
      const res = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      return res.json();
    },
    onSuccess: () => {
      // Invalida o cache e força refetch
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Client state com Zustand

Zustand (11M downloads/semana, +30% ano a ano) é para estado que existe só no client. Sem boilerplate, sem providers, sem reducers.

TYPESCRIPT
// Client state: UI state que não vem do servidor
// Zustand é minimalista e performático
import { create } from 'zustand';
 
interface UIState {
  sidebarOpen: boolean;
  theme: 'light' | 'dark';
  activeFilters: string[];
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
  toggleFilter: (filter: string) => void;
}
 
// Store sem provider, sem boilerplate
export const useUIStore = create<UIState>((set) => ({
  sidebarOpen: false,
  theme: 'light',
  activeFilters: [],
 
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
 
  setTheme: (theme) => set({ theme }),
 
  toggleFilter: (filter) =>
    set((state) => ({
      activeFilters: state.activeFilters.includes(filter)
        ? state.activeFilters.filter((f) => f !== filter)
        : [...state.activeFilters, filter],
    })),
}));
 
// Uso no componente: subscribe granular, sem re-render desnecessário
// Zustand re-renderiza APENAS componentes que usam o slice que mudou
function Sidebar() {
  const sidebarOpen = useUIStore((state) => state.sidebarOpen);
  const toggleSidebar = useUIStore((state) => state.toggleSidebar);
  // ...
}

Performance comparada de state managers

State ManagerRe-render 1000 subscribersBundle sizeBoilerplate
Zustand~12ms1.1 KBMínimo
Jotai~14ms2.4 KBMínimo (atômico)
Redux Toolkit~18ms11 KBModerado
Redux + Saga~22ms18 KBAlto

Em um dashboard de monitoramento que construí para um cliente de telecom em 2024, migrei de Redux Toolkit para Zustand + TanStack Query. O bundle do módulo de estado caiu de 34 KB para 8.2 KB. O tempo de re-render em telas com 200+ elementos caiu de 45ms para 16ms. A migração levou 4 dias para 2 devs. Se você está começando projeto novo com Redux em 2026, leia a seção de anti-patterns antes.

Build tools: Vite venceu, e agora?

Webpack dominou por 8 anos. Vite encerrou essa era. Os números são categóricos:

Bash
# Benchmark reproduzível: projeto React + TypeScript com 120 componentes
 
# Vite 6 (esbuild + Rollup)
$ time npm run dev
# Cold start: 2.8s
# HMR: 45ms (módulo individual)
# Production build: 8.2s
 
# Webpack 5 (equivalente CRA)
$ time npm start
# Cold start: 43s
# HMR: 2.1s (rebuild parcial)
# Production build: 34s

A diferença não é 2x. É 15x no cold start. Eu perdi anos da minha vida esperando Webpack rebuildar. Quando migrei o projeto principal da Agência Poti de Webpack para Vite em 2023, o ciclo de feedback do time caiu de "alt-tab, esperar, voltar" para "salvar e já ver". Produtividade medida em PRs por dia subiu 23% no primeiro mês.

Webpack ainda faz sentido?

Em um único cenário: projetos legados com configuração Webpack complexa (Module Federation, custom loaders proprietários) onde o custo de migração excede 2 sprints. Para projeto novo, não existe justificativa.

Turbopack: o wildcard

Turbopack, escrito em Rust pelo time da Vercel, é o bundler default do Next.js em dev mode desde o Next.js 15. Em produção, ainda usa Webpack internamente (Next.js 16 planeja migração completa). Se você já está no ecossistema Next.js, Turbopack chega sem esforço. Se está fora, Vite é a escolha. Escrevi mais sobre isso em Desbloqueie o Poder do Next.js com o Turbopack.

Evan You (criador do Vite) anunciou o Rolldown, um substituto do Rollup escrito em Rust, que será integrado ao Vite 7. A guerra dos bundlers em Rust está apenas começando, mas para o dev que usa a ferramenta, a interface não muda. npm run dev continua funcionando.

Testes: Vitest + Playwright como padrão

Vitest matou o Jest para projetos novos

Vitest roda no mesmo pipeline do Vite. Mesma config, mesmos transforms, mesmos aliases de path. O cold start é 5.6x mais rápido que o Jest porque não precisa de transformação Babel separada.

TYPESCRIPT
// vitest.config.ts - configuração mínima que herda do vite.config.ts
import { defineConfig } from 'vitest/config';
 
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',          // Mais rápido que istanbul
      reporter: ['text', 'json', 'html'],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
      },
    },
    // Watch mode inteligente: só roda testes de arquivos alterados
    // Jest faz isso também, mas Vitest é instantâneo por usar o HMR do Vite
  },
});
TYPESCRIPT
// Exemplo de teste com Vitest - API idêntica ao Jest
// Migração: trocar imports e config, testes ficam iguais
import { describe, it, expect, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUsers } from './useUsers';
 
function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}
 
describe('useUsers', () => {
  it('retorna lista de usuários do servidor', async () => {
    // vi.fn() é equivalente ao jest.fn()
    const mockUsers = [{ id: '1', name: 'Marcos', email: '[email protected]' }];
 
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockUsers),
    });
 
    const { result } = renderHook(() => useUsers(), {
      wrapper: createWrapper(),
    });
 
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data).toEqual(mockUsers);
  });
});

Migrei 340 testes de Jest para Vitest em um projeto Next.js. Tempo total: 6 horas (1 dev). O CI que rodava em 4min12s caiu para 1min48s. A migração é mecânica: troca jest.fn() por vi.fn(), ajusta config, e roda. Se você quer entender melhor o pipeline de CI, escrevi sobre isso em Como configurar CI/CD para Next.js na Vercel com GitHub Actions.

Playwright substituiu Cypress

Playwright atingiu 8M downloads/semana e 92% de satisfação. Cypress está em declínio. Os motivos são técnicos:

  • Playwright roda testes em paralelo por default (Cypress é serial)
  • Suporta múltiplos browsers nativamente (Chromium, Firefox, WebKit)
  • Não injeta código no browser (Cypress roda dentro do browser, limitando acesso a múltiplas tabs/origens)
TYPESCRIPT
// playwright.config.ts - configuração para projeto Next.js
import { defineConfig, devices } from '@playwright/test';
 
export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,        // Paralelo por default
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [['html'], ['json', { outputFile: 'results.json' }]],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',  // Trace completo só quando falha
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'mobile', use: { ...devices['Pixel 5'] } },
  ],
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

Performance real: o que medir e como

Bundle size é red herring se você não mede o que o usuário sente. As métricas que importam são as Core Web Vitals: LCP (Largest Contentful Paint), INP (Interaction to Next Paint), e CLS (Cumulative Layout Shift).

O que medir

MétricaBomRuimComo medir
LCP< 2.5s> 4.0sLighthouse, CrUX, web-vitals lib
INP< 200ms> 500msweb-vitals lib, Chrome DevTools
CLS< 0.1> 0.25Lighthouse, Layout Instability API
TTFB< 800ms> 1800msServer timing, Vercel Analytics
Bundle size (JS)< 100 KB gzip> 300 KB gzipnpx vite-bundle-visualizer

Code splitting que funciona

TYPESCRIPT
// Errado: importar tudo no bundle principal
import { Chart } from 'recharts';
import { DataGrid } from '@mui/x-data-grid';
import { Editor } from '@monaco-editor/react';
 
// Correto: lazy loading com Suspense por rota/feature
import { lazy, Suspense } from 'react';
 
const Chart = lazy(() => import('recharts').then(m => ({ default: m.BarChart })));
const DataGrid = lazy(() => import('@mui/x-data-grid').then(m => ({ default: m.DataGrid })));
const Editor = lazy(() => import('@monaco-editor/react'));
 
// Componente com fallback adequado (não um spinner genérico)
function DashboardChart({ data }: { data: ChartData[] }) {
  return (
    <Suspense fallback={<div className="h-[400px] animate-pulse bg-gray-100 rounded" />}>
      <Chart data={data} />
    </Suspense>
  );
}

Em um projeto de e-commerce, apliquei code splitting por rota e lazy loading para componentes pesados (editor de imagem, gráficos de analytics). O bundle inicial caiu de 420 KB para 89 KB gzip. O LCP em 3G simulado caiu de 4.8s para 1.9s. A técnica é simples. O impacto é brutal.

Para estratégias avançadas de cache que complementam code splitting, veja Caching no Next.js: ISR, On-Demand e Stale-While-Revalidate.

Deploy: Vercel vs AWS vs VPS

Decision tree por cenário

Text
PRECISA de edge functions + preview deploys + zero config?
├── SIM → Vercel ou Netlify
│   ├── Orçamento < R$500/mês → Vercel Pro (R$100/mês)
│   └── Orçamento > R$500/mês → Vercel Enterprise ou AWS
└── NÃO
    ├── Precisa de controle total sobre infra?
    │   ├── SIM → AWS (ECS/Fargate) ou VPS (Hetzner/DigitalOcean)
    │   │   ├── Time tem devops? → AWS com Terraform
    │   │   └── Sem devops? → VPS com Docker Compose
    │   └── NÃO → Vercel
    └── Projeto interno/intranet?
        └── VPS ou on-premise
CenárioRecomendaçãoCusto mensal estimadoComplexidade
Freelancer / portfolioVercel Hobby (free)R$0Baixa
Startup MVPVercel ProR$100-500Baixa
SaaS em crescimentoAWS ECS + CloudFrontR$800-3000Alta
Enterprise reguladoAWS/GCP com VPC dedicadaR$5000+Muito alta
Blog / docsVercel ou Cloudflare PagesR$0-50Mínima

Em 2024, migrei um SaaS de Vercel Pro para AWS ECS quando a conta chegou a R$2.800/mês. Na AWS, o mesmo tráfego custa R$1.100/mês, mas precisei de 3 semanas para configurar ECS, ALB, CloudFront, e Route 53. O break-even foi em 5 meses. Para quem quer entender o setup completo de infra, escrevi sobre Docker e CI/CD em DevOps para Devs.

O que NÃO fazer: anti-patterns que custam semanas

Anti-pattern 1: Redux para server state

TYPESCRIPT
// ❌ ERRADO: Redux gerenciando dados do servidor
// Resultado: 200+ linhas para um CRUD simples
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    data: [] as User[],
    loading: false,
    error: null as string | null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => { state.loading = true; })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message ?? 'Erro';
      });
    // Repita para create, update, delete...
    // + cache manual, + invalidação manual, + refetch manual
  },
});
 
// ✅ CORRETO: TanStack Query para server state
// Resultado: 15 linhas, cache automático, refetch automático
const { data, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
  staleTime: 5 * 60 * 1000,
});

Anti-pattern 2: useEffect com dependências erradas

TYPESCRIPT
// ❌ ERRADO: re-render infinito
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
 
  useEffect(() => {
    // Objeto criado a cada render → referência muda → loop infinito
    const options = { includeAvatar: true };
    fetchUser(userId, options).then(setUser);
  }, [userId, { includeAvatar: true }]); // ← objeto literal na dependência
 
  return <div>{user?.name}</div>;
}
 
// ✅ CORRETO: dependências estáveis
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
 
  useEffect(() => {
    const controller = new AbortController();
 
    fetchUser(userId, { includeAvatar: true, signal: controller.signal })
      .then(setUser)
      .catch((err) => {
        if (err.name !== 'AbortError') console.error(err);
      });
 
    // Cleanup: cancela request se userId mudar antes de resolver
    return () => controller.abort();
  }, [userId]); // ← primitivo estável
 
  return <div>{user?.name}</div>;
}
 
// ✅ MELHOR AINDA: TanStack Query elimina o useEffect
function UserProfile({ userId }: { userId: string }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId, { includeAvatar: true }),
  });
 
  return <div>{user?.name}</div>;
}

Esse anti-pattern de useEffect é o bug mais frequente que vejo em code review. Na Agência Poti, adicionei uma regra de ESLint customizada que flagga useEffect com fetch dentro. Se o dev quer buscar dados, usa TanStack Query. Sem exceção. Para mais sobre armadilhas do JavaScript que causam esse tipo de bug, veja Lacunas Ocultas do JavaScript Avançado.

Anti-pattern 3: SSR para tudo

Text
Página precisa de dados dinâmicos por request?
├── NÃO → SSG (Static Site Generation) ou ISR
│   ├── Conteúdo muda < 1x/hora → ISR com revalidate
│   └── Conteúdo estático → SSG puro
└── SIM
    ├── Dados são personalizados por usuário?
    │   ├── SIM → SSR ou CSR com auth
    │   └── NÃO → ISR com revalidate curto
    └── Precisa de SEO?
        ├── SIM → SSR
        └── NÃO → CSR (SPA) é suficiente

Eu vi um time usar SSR para um dashboard interno que só 12 pessoas acessavam. Cada page load batia no servidor, renderizava HTML, hidratava no client. O TTFB era 1.8s. Migrei para CSR puro com TanStack Query. TTFB caiu para 180ms (servindo um HTML estático) e os dados carregavam em 340ms via API. Para entender quando Server Components fazem sentido, veja Server Components no Next.js 16.

Anti-pattern 4: console.log como debug

TYPESCRIPT
// ❌ ERRADO: console.log espalhado pelo código
async function processOrder(order: Order) {
  console.log('order', order);
  console.log('processing...');
  const result = await chargePayment(order);
  console.log('result', result);
  console.log('done');
  return result;
}
 
// ✅ CORRETO: logging estruturado com níveis
import pino from 'pino';
 
const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty' }
    : undefined,
});
 
async function processOrder(order: Order) {
  logger.info({ orderId: order.id, total: order.total }, 'Iniciando processamento');
 
  const result = await chargePayment(order);
 
  if (result.success) {
    logger.info({ orderId: order.id, chargeId: result.chargeId }, 'Pagamento aprovado');
  } else {
    logger.error({ orderId: order.id, error: result.error }, 'Pagamento recusado');
  }
 
  return result;
}

Em produção, console.log é ruído. Não tem nível, não tem estrutura, não é pesquisável. Quando um bug acontece às 3h da manhã, você precisa de logs estruturados que o Datadog/Grafana/CloudWatch consegue filtrar. Escrevi sobre observabilidade em backend em Engenharia Backend com Node.js e TypeScript.

Anti-pattern 5: não configurar ESLint/Prettier no dia 1

O custo de adicionar linting em projeto existente cresce exponencialmente. Em projeto novo, leva 15 minutos. Em projeto com 6 meses de código sem padrão, leva 2-3 dias de fixes.

Bash
# Setup no dia 1 de qualquer projeto - 15 minutos
npm install -D eslint @eslint/js typescript-eslint prettier eslint-config-prettier
 
# eslint.config.ts (flat config, padrão desde ESLint 9)
TYPESCRIPT
// eslint.config.ts
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
 
export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  prettier,
  {
    rules: {
      // Decisão: forçar retorno explícito em funções
      '@typescript-eslint/explicit-function-return-type': 'error',
      // Decisão: proibir any (usar unknown + type guard)
      '@typescript-eslint/no-explicit-any': 'error',
      // Decisão: proibir console.log (usar logger)
      'no-console': ['error', { allow: ['warn', 'error'] }],
    },
  },
);

Para mais sobre lacunas que devs ignoram em configuração de projetos, incluindo npm e segurança de dependências, tenho um artigo dedicado.

Roadmap progressivo: 5 níveis de maturidade

Esse roadmap não é linear. Você não precisa "completar" um nível para ir ao próximo. Mas cada nível pressupõe que o anterior está sólido.

Nível 1: Fundamentos (0-6 meses)

Foco: HTML semântico, CSS (Flexbox, Grid, Custom Properties), JavaScript (closures, promises, event loop).

Sem framework. Sem TypeScript. Sem build tool. Abra um arquivo .html, escreva JavaScript vanilla, e entenda o que o browser faz. Eu sei que é tentador pular para React no dia 1. Resista. Os devs que mais crescem rápido no meu time são os que entendem o DOM antes de abstraí-lo.

Recurso: CSS Custom Properties para entender variáveis nativas antes de usar Tailwind.

Nível 2: Ferramentas e tipagem (6-12 meses)

Foco: TypeScript (tipos utilitários, generics, discriminated unions), Git (rebase, cherry-pick, bisect), terminal, npm/pnpm.

TypeScript não é "JavaScript com tipos". É uma linguagem de modelagem que compila para JavaScript. A diferença importa. Quando você modela um domínio com discriminated unions, o compilador elimina categorias inteiras de bugs em compile time.

TYPESCRIPT
// Nível 2: modelar estados impossíveis como irrepresentáveis
// Isso não é "tipar por tipar". É eliminar bugs em compile time.
 
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };
 
// O compilador GARANTE que você só acessa `data` quando status é 'success'
function renderState<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case 'idle': return 'Aguardando...';
    case 'loading': return 'Carregando...';
    case 'success': return `Dados: ${JSON.stringify(state.data)}`;
    case 'error': return `Erro: ${state.error.message}`;
  }
}

Aprofunde-se em Design Patterns que Todo Dev Senior Deveria Dominar em TypeScript.

Nível 3: Framework + ecossistema (12-24 meses)

Foco: Um framework (React, Vue ou Svelte), seu meta-framework (Next.js, Nuxt, SvelteKit), TanStack Query, Zustand/Pinia, Tailwind CSS, Vitest, Playwright.

Escolha UM framework usando o sistema de decisão da seção 2. Domine-o. Não fique pulando entre React e Vue a cada 3 meses. O State of JS confirma: 2.6 frameworks por carreira. Profundidade supera amplitude.

Nível 4: Arquitetura e backend (24-48 meses)

Foco: APIs REST e GraphQL, banco de dados (PostgreSQL), ORM (Prisma ou Drizzle), autenticação, rate limiting, connection pooling, arquitetura hexagonal.

Aqui você deixa de ser "dev frontend" e vira "dev web". Entender o backend muda como você projeta o frontend. Quando você sabe que a API tem rate limit de 100 req/s, você implementa debounce no client. Quando entende connection pooling, para de culpar o ORM pela lentidão.

Para comparar ORMs com benchmarks reais, veja Prisma ORM vs Drizzle ORM.

Nível 5: Escala e operação (48+ meses)

Foco: Docker, CI/CD, Kubernetes, observabilidade (logs, métricas, traces), feature flags, testes de carga, WebSockets vs SSE, Server Actions.

Neste nível, a pergunta muda de "como fazer funcionar" para "como fazer funcionar para 10.000 usuários simultâneos sem acordar às 3h da manhã". Observabilidade não é luxo. É o que separa "funciona no meu computador" de "funciona em produção".

Segurança, SEO técnico e observabilidade

Essas três áreas são ignoradas por devs júnior e cobradas por devs sênior em code review. Não são opcionais.

Segurança mínima viável

TYPESCRIPT
// Headers de segurança - configurar no middleware do Next.js ou no servidor
// next.config.ts
const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-XSS-Protection', value: '1; mode=block' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload',
  },
];

Além de headers, valide TODA entrada do usuário no servidor. Zod é a biblioteca padrão para isso em TypeScript:

TYPESCRIPT
import { z } from 'zod';
 
// Validação no servidor - nunca confie no client
const CreateUserSchema = z.object({
  name: z.string().min(2).max(100).trim(),
  email: z.string().email().toLowerCase(),
  age: z.number().int().min(18).max(120),
});
 
// No handler da API
export async function POST(req: Request) {
  const body = await req.json();
  const parsed = CreateUserSchema.safeParse(body);
 
  if (!parsed.success) {
    return Response.json({ errors: parsed.error.flatten() }, { status: 400 });
  }
 
  // parsed.data é tipado e validado
  const user = await db.user.create({ data: parsed.data });
  return Response.json(user, { status: 201 });
}

Aprofunde-se em 5 Lacunas Críticas que Todo Dev JavaScript Ignora em APIs, npm e Open Source.

SEO técnico para devs

SEO não é magia de marketing. É engenharia. Os fatores que você controla como dev:

  • TTFB < 800ms: use SSG/ISR para páginas estáticas, edge functions para dinâmicas
  • LCP < 2.5s: otimize imagens (next/image, sharp), code splitting, font-display: swap
  • Meta tags dinâmicas: título único por página, description de 150-160 caracteres, Open Graph
  • Sitemap e robots.txt: gerados automaticamente no build
  • Dados estruturados: JSON-LD para artigos, produtos, FAQs

Observabilidade

A tríade é: logs (o que aconteceu), métricas (quanto aconteceu), traces (como aconteceu). Em frontend, a web-vitals lib reporta Core Web Vitals para seu backend de analytics. Em backend, Pino + OpenTelemetry cobrem logs e traces.

TYPESCRIPT
// Reportar Web Vitals para analytics
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
 
function sendToAnalytics(metric: { name: string; value: number; id: string }) {
  // Enviar para seu endpoint de analytics (não Google Analytics)
  fetch('/api/vitals', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: metric.name,
      value: Math.round(metric.value),
      id: metric.id,
      page: window.location.pathname,
      timestamp: Date.now(),
    }),
  });
}
 
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

Cenários reais: qual stack para qual situação

Freelancer / dev solo

Text
Stack: Next.js + Zustand + TanStack Query + Tailwind + shadcn/ui + Vitest
Deploy: Vercel (free tier → Pro quando faturar)
Motivo: maior mercado de vagas, mais tutoriais, deploy em 1 comando
Salário médio BR: R$8.000-14.000 (pleno), R$15.000-25.000 (sênior)

Eu comecei como freelancer em 2005. Se tivesse que recomeçar hoje, iria de Next.js sem pensar duas vezes. O ecossistema é imbatível para quem trabalha sozinho.

Startup MVP (< 6 meses para mercado)

Text
Stack: Vite + React + Zustand + TanStack Query + shadcn/ui + Supabase
Deploy: Vercel Pro + Supabase (Postgres gerenciado)
Motivo: velocidade de desenvolvimento máxima, custo baixo, fácil de contratar
Time ideal: 2-3 devs fullstack

Em 2024, ajudei uma startup de saúde a ir do zero ao MVP em 11 semanas com essa stack. O custo de infra no primeiro mês foi R$0 (free tiers). No sexto mês, com 2.000 usuários ativos, o custo era R$340/mês.

Enterprise / banco / fintech regulada

Text
Stack: Angular 18+ + RxJS + NgRx + Angular Material + Jasmine/Karma ou Jest
Deploy: AWS com VPC dedicada, ECS, CloudFront
Motivo: conventions fortes, tipagem rígida, compliance, equipe grande (15+ devs)
Salário médio BR: R$12.000-18.000 (pleno), R$20.000-35.000 (sênior)

O moat do Angular em bancos brasileiros é real. Se você quer trabalhar no Itaú, Bradesco, ou TOTVS, Angular é o caminho. O ecossistema RxJS + NgRx é complexo, mas para apps com centenas de telas e dezenas de devs, a rigidez compensa.

Projeto content-first (blog, docs, marketing)

Text
Stack: Astro + React islands (quando precisa de interatividade) + Tailwind
Deploy: Vercel ou Cloudflare Pages
Motivo: zero JS por default, 39 pontos acima de Next.js em satisfação
Performance: LCP < 1s em páginas estáticas

Aprendizado / projeto pessoal

Text
Stack: SvelteKit + Tailwind + Vitest
Motivo: maior satisfação de devs (88%), sintaxe limpa, boa para aprender
Caveat: mercado BR ainda pequeno (~120 vagas), use para projetos pessoais

A frase que resume o sentimento da comunidade: "Devs get paid to write React but enjoy writing Svelte." Se você já domina React e quer expandir, SvelteKit é a escolha para projetos pessoais.

FAQ

Preciso trocar de framework a cada 2 anos?

Não. O State of JS 2025 mostra que a média é 2.6 frameworks por carreira inteira. Escolha um, domine, e só troque quando o mercado ou o projeto exigir. Framework hopping é a forma mais eficiente de nunca ficar sênior em nada.

TypeScript é obrigatório em 2026?

Sim. Todo projeto novo em 2026 que não usa TypeScript está acumulando dívida técnica desde o dia 1. Não é opinião. É o padrão da indústria. React, Vue, Svelte, Next.js, Astro, todos têm TypeScript como first-class citizen.

Redux ainda vale a pena?

Para projetos novos, não. TanStack Query resolve server state. Zustand resolve client state. Redux Toolkit ainda funciona em projetos existentes, mas o boilerplate não se justifica para projetos greenfield. Se você tem um projeto legado com Redux, migre incrementalmente: substitua os slices de server state por TanStack Query primeiro.

Webpack ou Vite para projeto novo?

Vite. Sem exceção. Cold start de 3s vs 45s. HMR de 45ms vs 2.1s. A única razão para manter Webpack é projeto legado com configuração complexa que custaria mais de 2 sprints para migrar.

Next.js ou Remix?

Next.js. O ecossistema é 5x maior, a Vercel investe pesado, e o mercado de vagas BR é incomparável. Remix tem boas ideias (loaders, actions), mas muitas delas foram absorvidas pelo Next.js App Router com Server Actions. Para entender Server Actions em profundidade, veja Revolucione sua Aplicação React com Next.js 15 Server Actions.

Devo aprender backend como dev frontend?

Sim. Entender o backend muda como você projeta o frontend. Saber que a API tem latência de 200ms muda como você implementa loading states. Saber que o banco tem connection pooling muda como você projeta queries. O guia de engenharia backend com Node.js e TypeScript é um bom ponto de partida.

Svelte é melhor que React?

Depende do critério. Satisfação do dev? Svelte ganha (88% vs 69%). Tamanho de mercado? React ganha por 30x. Bundle size em apps pequenos? Svelte ganha. Bundle size em apps com 19+ componentes? React empata ou ganha. A resposta correta é: Svelte é melhor para projetos pessoais e apps pequenos. React é melhor para empregabilidade e ecossistema.

Como organizar CSS em 2026?

Utility-first com Tailwind CSS é o padrão dominante. Se você vem de BEM ou OOCSS, a transição é estranha por 2 semanas e depois você não volta. Para entender as alternativas e quando cada uma faz sentido, escrevi uma comparação detalhada em BEM, OOCSS ou Utility-First.

Qual banco de dados para começar?

PostgreSQL. É o banco relacional mais completo, tem suporte a JSON (substitui MongoDB em 90% dos casos), e é o default de Supabase, Railway, Neon, e Vercel Postgres. Quando precisar escalar, connection pooling com PgBouncer resolve.

Jest ou Vitest?

Vitest para projeto novo. A API é praticamente idêntica ao Jest, o cold start é 5.6x mais rápido, e a integração com Vite elimina configuração de transformação. Para projetos existentes com Jest, migre quando tiver oportunidade. A migração é mecânica e leva menos de 1 dia para a maioria dos projetos.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.