Javascript

Revisão + Projeto Final: Produção Real Já leu

22 min de leitura

Revisão  + Projeto Final: Produção Real
Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança e performance — os três pilares que separam um projeto que "funciona no meu computador" de uma aplicação profissional em produção. Este artigo

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança e performance — os três pilares que separam um projeto que "funciona no meu computador" de uma aplicação profissional em produção.

Este artigo final do módulo tem dois objetivos. Primeiro, consolidar os conceitos com uma revisão estruturada. Segundo, integrar tudo em um projeto completo que representa o estado da arte de uma aplicação Node.js + React em produção: pipeline de CI/CD, segurança em camadas, performance monitorada e deploy automatizado.


O que vimos


Deploy

Deploy é o processo de levar código do computador do desenvolvedor para um servidor acessível na internet. O ponto central é que front-end e back-end têm naturezas diferentes e precisam de estratégias diferentes.

O front-end React é compilado em arquivos estáticos que qualquer servidor pode entregar. A Vercel e a Netlify detectam automaticamente projetos Vite, executam o build e servem os arquivos em uma CDN global. A única configuração necessária é o vercel.json ou netlify.toml para garantir que todas as rotas retornem index.html — fundamental para SPAs com React Router.

O back-end Node.js é um processo que precisa rodar continuamente em um servidor. O Railway faz isso de forma simples: conecta ao GitHub, detecta o projeto Node, e executa npm start. O banco de dados vive no MongoDB Atlas, que oferece um cluster gratuito suficiente para desenvolvimento e projetos pequenos.

O CI/CD com GitHub Actions fecha o ciclo: cada push para main dispara automaticamente os testes e, se passarem, o deploy — eliminando deploys manuais e garantindo que código com problemas nunca chegue à produção.

Fluxo completo de um deploy profissional:

git push origin main
       ↓
GitHub Actions: npm ci → npm run quality → npm test
       ↓ (somente se tudo passar)
Railway: npm run build → npm start
MongoDB Atlas: disponível 24/7
       ↓
Vercel: detecta push → npm run build → CDN global
       ↓
Usuário acessa https://meuapp.com.br

Segurança

Segurança é uma mentalidade, não uma checklist. O princípio fundamental é tratar toda entrada de dados como não confiável e defender em múltiplas camadas — se uma falhar, as outras ainda protegem.

As vulnerabilidades mais comuns e suas defesas:

Injeção NoSQL    → validar tipos, nunca concatenar queries
XSS              → React escapa por padrão; DOMPurify quando necessário
Auth fraca       → bcrypt fator 12, JWT com expiração, timing-safe compare
Força bruta      → rate limiting em /login (5 req/15min)
CORS aberto      → restringir às origens conhecidas em produção
Dados expostos   → select: false, Omit<T, 'senha'>, projeção no MongoDB
Deps vulneráveis → npm audit no CI/CD, atualizações regulares
Headers inseguros → Helmet com CSP configurado

Performance

Performance se mede, não se acha. O Lighthouse dá uma baseline, o DevTools Profiler localiza o problema, e a intervenção cirúrgica resolve com mínimo de complexidade adicionada.

Front-end — os maiores ganhos:
  Bundle size    → code splitting, lazy loading, manualChunks
  Re-renders     → React.memo + useCallback para listas com callbacks
  Listas longas  → virtualização com TanStack Virtual
  Imagens        → loading="lazy", fetchPriority="high" para LCP

Back-end — os maiores ganhos:
  Queries lentas → índices compostos (IXSCAN vs COLLSCAN)
  Dados em excesso → .lean() + .select() nas queries de leitura
  Round-trips    → Promise.all() para queries independentes
  Cálculos repetidos → cache em memória com TTL
  Banda de rede  → compressão gzip/brotli

O projeto final — aplicação em estado de produção

Vamos construir a versão de produção da SPA de gestão, adicionando tudo que aprendemos no Módulo 7 sobre a base do Módulo 6. O foco é mostrar como as camadas se encaixam — não reescrever o que já existe, mas elevar a qualidade.


Estrutura final do projeto

spa-gestao/
├── .github/
│   └── workflows/
│       ├── ci.yml          ← testes e qualidade em PRs
│       └── deploy.yml      ← deploy em push para main
├── apps/
│   ├── api/                ← back-end Node.js
│   │   ├── src/
│   │   │   ├── config/
│   │   │   │   ├── index.js
│   │   │   │   └── database.js
│   │   │   ├── middlewares/
│   │   │   │   ├── auth.js
│   │   │   │   ├── erros.js
│   │   │   │   ├── validar.js
│   │   │   │   ├── performance.js
│   │   │   │   └── rateLimiter.js
│   │   │   ├── models/
│   │   │   │   ├── Usuario.js
│   │   │   │   ├── Produto.js
│   │   │   │   └── Tarefa.js
│   │   │   ├── routes/
│   │   │   │   ├── auth.js
│   │   │   │   ├── produtos.js
│   │   │   │   └── tarefas.js
│   │   │   └── index.js
│   │   ├── tests/
│   │   ├── .env.example
│   │   └── package.json
│   └── web/                ← front-end React
│       ├── src/
│       │   ├── components/
│       │   ├── hooks/
│       │   ├── pages/
│       │   ├── services/
│       │   ├── stores/
│       │   └── utils/
│       │       └── webVitals.js
│       ├── public/
│       ├── vercel.json
│       ├── vite.config.js
│       └── package.json
└── README.md

Back-end — camadas de produção integradas

Vamos construir o index.js da API com todas as camadas do Módulo 7 integradas de forma coesa. A ordem dos middlewares importa — cada um tem um papel específico no pipeline de requisição.

// apps/api/src/index.js
// Cada middleware tem um propósito claro e uma posição específica na cadeia.
// A ordem não é arbitrária: segurança vem antes de lógica,
// compressão antes de resposta, erros depois de tudo.

require('dotenv').config();

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const config = require('./config');
const { conectar } = require('./config/database');
const { monitorarPerformance } = require('./middlewares/performance');
const { limiteGeral, limiteAuth } = require('./middlewares/rateLimiter');

const app = express();

// ── 1. Segurança — headers HTTP de proteção ──────────
// Helmet deve vir antes de qualquer rota para garantir
// que todas as respostas tenham os headers corretos
app.use(helmet());
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      connectSrc: ["'self'", config.frontendUrl],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      objectSrc: ["'none'"],
    },
  })
);

// ── 2. CORS — controle de origens permitidas ─────────
const origensPermitidas = [
  config.frontendUrl,
  ...(config.eDev ? ['http://localhost:5173', 'http://localhost:4173'] : []),
].filter(Boolean); // remove undefined se FRONTEND_URL não estiver configurada

app.use(
  cors({
    origin: (origin, callback) => {
      // Permite requisições sem origin (Postman, curl, apps mobile)
      if (!origin || origensPermitidas.includes(origin)) {
        return callback(null, true);
      }
      callback(new Error(`Origem bloqueada pelo CORS: ${origin}`));
    },
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
    maxAge: 86400,
  })
);

// ── 3. Compressão — reduz tráfego de rede ───────────
// Vem antes das rotas para comprimir todas as respostas
app.use(compression({ threshold: 1024, level: 6 }));

// ── 4. Rate limiting global ──────────────────────────
app.use(limiteGeral);

// ── 5. Parse do corpo da requisição ─────────────────
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// ── 6. Monitoramento de performance ─────────────────
// Loga requisições lentas (> 500ms) e adiciona Server-Timing em dev
app.use(monitorarPerformance);

// ── 7. Health check — sem autenticação ──────────────
// Primeiro endpoint real — antes das rotas protegidas
// Plataformas de hospedagem (Railway, Fly.io) usam este endpoint
// para verificar se a aplicação está viva
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    versao: process.env.npm_package_version || '1.0.0',
    ambiente: config.ambiente,
    uptime: `${Math.floor(process.uptime())}s`,
    timestamp: new Date().toISOString(),
    // Informações de memória úteis para detectar memory leaks
    memoria: {
      usada: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
      total: `${Math.round(process.memoryUsage().heapTotal / 1024 / 1024)}MB`,
    },
  });
});

// ── 8. Rotas da API ──────────────────────────────────
// Rate limiting específico aplicado apenas nas rotas de autenticação
app.use('/auth', limiteAuth, require('./routes/auth'));
app.use('/produtos', require('./routes/produtos'));
app.use('/tarefas', require('./routes/tarefas'));

// ── 9. Tratamento de erros — sempre por último ───────
// O handler de 404 captura rotas não definidas
// O handler de erros captura todos os erros lançados nas rotas
const { naoEncontrado, tratadorDeErros } = require('./middlewares/erros');
app.use(naoEncontrado);
app.use(tratadorDeErros);

// ── Inicialização ────────────────────────────────────
async function iniciar() {
  try {
    await conectar();
    const servidor = app.listen(config.porta, () => {
      console.info(
        `[Server] ✅ Rodando na porta ${config.porta} (${config.ambiente})`
      );
    });

    // Encerramento gracioso: aguarda requisições em andamento antes de parar.
    // O Railway envia SIGTERM antes de reiniciar o container.
    // Sem isso, requisições em processamento seriam abortadas abruptamente.
    process.on('SIGTERM', () => {
      console.info('[Server] SIGTERM recebido. Encerrando graciosamente...');
      servidor.close(() => {
        console.info('[Server] Conexões fechadas. Processo encerrado.');
        process.exit(0);
      });
    });
  } catch (erro) {
    console.error('[Server] ❌ Falha ao iniciar:', erro.message);
    process.exit(1);
  }
}

iniciar();
module.exports = app; // exporta para os testes

Agora os middlewares de segurança e rate limiting centralizados:

// apps/api/src/middlewares/rateLimiter.js
// Separar em arquivo próprio facilita ajustar os limites sem tocar no index.js
const rateLimit = require('express-rate-limit');

// Mensagem padrão — retorna JSON consistente com o restante da API
function mensagemPadrao(limite, janela) {
  return {
    erro: `Muitas requisições. Limite: ${limite} por ${janela} minutos.`,
    status: 429,
  };
}

// Limite geral: 100 requisições por 15 minutos
// Protege contra bots e scrapers indiscriminados
const limiteGeral = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: mensagemPadrao(100, 15),
});

// Limite de autenticação: 5 tentativas por 15 minutos
// Protege contra ataques de força bruta em senhas
// skipSuccessfulRequests: logins bem-sucedidos não contam — só falhas
const limiteAuth = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  standardHeaders: true,
  legacyHeaders: false,
  message: mensagemPadrao(5, 15),
});

module.exports = { limiteGeral, limiteAuth };
// apps/api/src/middlewares/erros.js
// Tratamento centralizado de erros — captura tudo que chegar aqui
// e transforma em respostas JSON consistentes

const config = require('../config');

// Captura rotas não definidas — deve vir após todas as rotas
function naoEncontrado(req, res) {
  res.status(404).json({
    erro: `Rota não encontrada: ${req.method} ${req.path}`,
    status: 404,
  });
}

// Handler global de erros — Express identifica pelo número de parâmetros (4)
// Deve ser o ÚLTIMO middleware registrado no app
function tratadorDeErros(erro, req, res, next) {
  // Log completo em desenvolvimento, mínimo em produção
  // (nunca exponha stack traces para o cliente em produção)
  if (config.eDev) {
    console.error('[Erro]', erro);
  } else {
    console.error(`[Erro] ${erro.message}`);
  }

  // Erros de validação do Mongoose — campos obrigatórios faltando, etc.
  if (erro.name === 'ValidationError') {
    const detalhes = Object.values(erro.errors).map((e) => e.message);
    return res.status(422).json({ erro: 'Dados inválidos.', detalhes, status: 422 });
  }

  // ID MongoDB com formato inválido (ex: 'abc' no lugar de ObjectId)
  if (erro.name === 'CastError') {
    return res.status(400).json({ erro: 'ID inválido.', status: 400 });
  }

  // Violação de campo único (ex: email duplicado)
  if (erro.code === 11000) {
    const campo = Object.keys(erro.keyValue)[0];
    return res.status(409).json({
      erro: `${campo} já está em uso.`,
      status: 409,
    });
  }

  // Token JWT expirado — o front-end deve redirecionar para /login
  if (erro.name === 'TokenExpiredError') {
    return res.status(401).json({ erro: 'Sessão expirada. Faça login novamente.', status: 401 });
  }

  // Token JWT malformado ou assinatura inválida
  if (erro.name === 'JsonWebTokenError') {
    return res.status(401).json({ erro: 'Token inválido.', status: 401 });
  }

  // Qualquer outro erro — resposta genérica em produção para não vazar detalhes
  const status = erro.statusCode || erro.status || 500;
  res.status(status).json({
    erro: config.eProd ? 'Erro interno do servidor.' : erro.message,
    status,
    // Stack trace apenas em desenvolvimento
    ...(config.eDev && { stack: erro.stack }),
  });
}

module.exports = { naoEncontrado, tratadorDeErros };

Rota com todas as camadas integradas

Vamos ver como uma rota completa se parece com autenticação, validação, cache e índices trabalhando juntos.

// apps/api/src/routes/tarefas.js
// Esta rota demonstra as camadas em ação:
// autenticação → validação → cache → query otimizada → resposta

const express = require('express');
const mongoose = require('mongoose');
const { z } = require('zod');
const NodeCache = require('node-cache');
const { autenticar } = require('../middlewares/auth');
const { validar } = require('../middlewares/validar');
const Tarefa = require('../models/Tarefa');

const router = express.Router();

// Cache de estatísticas — TTL de 2 minutos
// Estatísticas são caras de calcular mas não precisam ser em tempo real
const cacheStats = new NodeCache({ stdTTL: 120 });

// Schemas de validação — Zod garante tipos e formatos corretos
const schemaCriar = z.object({
  titulo: z.string().min(2).max(200).trim(),
  descricao: z.string().max(2000).trim().optional(),
  prioridade: z.enum(['baixa', 'media', 'alta']).default('media'),
  prazo: z.string().datetime().optional(),
  tags: z.array(z.string().trim()).max(10).optional(),
});

const schemaAtualizar = schemaCriar.partial(); // todos os campos opcionais

// ── GET /tarefas — listagem otimizada ────────────────
router.get('/', autenticar, async (req, res, next) => {
  try {
    const {
      status,
      prioridade,
      busca,
      pagina = '1',
      por_pagina = '10',
      ordenar = '-criadoEm',
    } = req.query;

    // Monta filtros — sempre incluindo o usuário autenticado
    // Isso garante isolamento de dados entre usuários
    const filtros = { usuario: req.usuario._id };
    if (status) filtros.status = status;
    if (prioridade) filtros.prioridade = prioridade;
    if (busca) filtros.$text = { $search: busca };

    const paginaNum = Math.max(1, parseInt(pagina, 10));
    const porPaginaNum = Math.min(50, Math.max(1, parseInt(por_pagina, 10)));
    const skip = (paginaNum - 1) * porPaginaNum;

    // Promise.all executa count e find em paralelo — economiza uma ida ao banco
    const [tarefas, total] = await Promise.all([
      Tarefa.find(filtros)
        .sort(ordenar)
        .skip(skip)
        .limit(porPaginaNum)
        // .select() reduz dados transferidos do banco para a API
        .select('titulo status prioridade prazo tags criadoEm')
        // .lean() retorna objetos JS simples — mais rápido que documentos Mongoose
        .lean(),
      Tarefa.countDocuments(filtros),
    ]);

    res.json({
      dados: tarefas,
      paginacao: {
        total,
        pagina: paginaNum,
        por_pagina: porPaginaNum,
        total_paginas: Math.ceil(total / porPaginaNum),
      },
    });
  } catch (erro) {
    next(erro);
  }
});

// ── GET /tarefas/estatisticas — com cache ────────────
router.get('/estatisticas', autenticar, async (req, res, next) => {
  try {
    const usuarioId = req.usuario._id.toString();
    const chaveCache = `stats:${usuarioId}`;

    // Tenta o cache primeiro — retorna instantaneamente se houver hit
    const emCache = cacheStats.get(chaveCache);
    if (emCache) return res.json(emCache);

    // Cache miss — executa a aggregation (operação custosa)
    const stats = await Tarefa.aggregate([
      // $match usa o índice { usuario: 1, status: 1 } — IXSCAN eficiente
      { $match: { usuario: new mongoose.Types.ObjectId(usuarioId) } },
      {
        $group: {
          _id: '$status',
          total: { $sum: 1 },
          comPrazo: {
            $sum: { $cond: [{ $ifNull: ['$prazo', false] }, 1, 0] },
          },
        },
      },
      {
        $project: {
          status: '$_id',
          total: 1,
          comPrazo: 1,
          _id: 0,
        },
      },
    ]);

    const resposta = { categorias: stats, geradoEm: new Date().toISOString() };

    // Armazena no cache — próximas requisições nos próximos 2 min são instantâneas
    cacheStats.set(chaveCache, resposta);
    res.json(resposta);
  } catch (erro) {
    next(erro);
  }
});

// ── POST /tarefas — criação com validação ────────────
router.post('/', autenticar, validar(schemaCriar), async (req, res, next) => {
  try {
    // req.body já foi validado e sanitizado pelo middleware validar()
    const tarefa = await Tarefa.create({
      ...req.body,
      usuario: req.usuario._id, // sempre vincula ao usuário autenticado
    });

    // Invalida o cache de estatísticas — dados mudaram
    cacheStats.del(`stats:${req.usuario._id}`);

    res.status(201).json(tarefa);
  } catch (erro) {
    next(erro);
  }
});

// ── PUT /tarefas/:id — atualização com ownership check ─
router.put('/:id', autenticar, validar(schemaAtualizar), async (req, res, next) => {
  try {
    const { id } = req.params;

    if (!mongoose.isValidObjectId(id)) {
      return res.status(400).json({ erro: 'ID inválido.', status: 400 });
    }

    // findOne com filtro de usuário — garante que só o dono pode editar
    // Retorna 404 (não 403) para não confirmar que o recurso existe
    const tarefa = await Tarefa.findOneAndUpdate(
      { _id: id, usuario: req.usuario._id }, // ownership check implícito
      { $set: req.body },
      { new: true, runValidators: true }
    );

    if (!tarefa) {
      return res.status(404).json({ erro: 'Tarefa não encontrada.', status: 404 });
    }

    cacheStats.del(`stats:${req.usuario._id}`);
    res.json(tarefa);
  } catch (erro) {
    next(erro);
  }
});

// ── DELETE /tarefas/:id ──────────────────────────────
router.delete('/:id', autenticar, async (req, res, next) => {
  try {
    const { id } = req.params;

    if (!mongoose.isValidObjectId(id)) {
      return res.status(400).json({ erro: 'ID inválido.', status: 400 });
    }

    const tarefa = await Tarefa.findOneAndDelete({
      _id: id,
      usuario: req.usuario._id, // ownership check
    });

    if (!tarefa) {
      return res.status(404).json({ erro: 'Tarefa não encontrada.', status: 404 });
    }

    cacheStats.del(`stats:${req.usuario._id}`);
    res.json({ mensagem: `Tarefa "${tarefa.titulo}" removida com sucesso.` });
  } catch (erro) {
    next(erro);
  }
});

module.exports = router;

Front-end — performance e monitoramento integrados

// apps/web/vite.config.js
// Configuração de produção com análise de bundle e code splitting otimizado
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    // Visualizador apenas quando analisando o bundle (npm run build -- --mode analyze)
    mode === 'analyze' &&
      visualizer({
        open: true,
        gzipSize: true,
        brotliSize: true,
        filename: 'bundle-analysis.html',
      }),
  ].filter(Boolean),

  build: {
    // Avisa se algum chunk ultrapassar 500KB antes de gzip
    chunkSizeWarningLimit: 500,

    rollupOptions: {
      output: {
        // Nomeia os chunks com o nome do módulo para facilitar debugging
        // e melhorar cache — vendor-react só muda quando React atualiza
        manualChunks: {
          'vendor-react': ['react', 'react-dom'],
          'vendor-router': ['react-router-dom'],
          'vendor-query': ['@tanstack/react-query'],
          'vendor-store': ['zustand'],
        },
      },
    },
  },

  // Proxy em desenvolvimento — evita problemas de CORS ao desenvolver
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
  },
}));
// apps/web/src/utils/webVitals.js
// Monitora e reporta Core Web Vitals em produção
// Dados reais de usuários são mais valiosos que dados de laboratório
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

// Classifica a métrica como "good", "needs-improvement" ou "poor"
// com base nos limites definidos pelo Google
function classificarMetrica(name, value) {
  const limites = {
    LCP:  { good: 2500,  poor: 4000  },
    INP:  { good: 200,   poor: 500   },
    CLS:  { good: 0.1,   poor: 0.25  },
    FCP:  { good: 1800,  poor: 3000  },
    TTFB: { good: 800,   poor: 1800  },
  };

  const limite = limites[name];
  if (!limite) return 'unknown';
  if (value <= limite.good) return 'good';
  if (value <= limite.poor) return 'needs-improvement';
  return 'poor';
}

function reportar(metrica) {
  const classificacao = classificarMetrica(metrica.name, metrica.value);
  const valor = metrica.name === 'CLS'
    ? metrica.value.toFixed(3)      // CLS é adimensional
    : `${Math.round(metrica.value)}ms`; // as demais são em ms

  // Log colorido para facilitar debugging em produção
  const cor = classificacao === 'good' ? '✅' : classificacao === 'needs-improvement' ? '⚠️' : '❌';
  console.log(`[Web Vitals] ${cor} ${metrica.name}: ${valor} (${classificacao})`);

  // Em produção, envie para o seu serviço de analytics
  // Exemplo com fetch para endpoint próprio de métricas:
  if (import.meta.env.PROD && import.meta.env.VITE_METRICS_URL) {
    navigator.sendBeacon(
      import.meta.env.VITE_METRICS_URL,
      JSON.stringify({
        name: metrica.name,
        value: metrica.value,
        classificacao,
        id: metrica.id,
        url: window.location.pathname,
        timestamp: Date.now(),
      })
    );
  }
}

export function iniciarMonitoramento() {
  onCLS(reportar);   // Cumulative Layout Shift
  onINP(reportar);   // Interaction to Next Paint
  onLCP(reportar);   // Largest Contentful Paint
  onFCP(reportar);   // First Contentful Paint
  onTTFB(reportar);  // Time to First Byte (velocidade do servidor)
}
// apps/web/src/main.jsx — integrando o monitoramento
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from './App';
import { iniciarMonitoramento } from './utils/webVitals';
import './index.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,
      gcTime: 1000 * 60 * 10,
      retry: 2,
      refetchOnWindowFocus: true,
    },
    mutations: {
      // Em caso de erro de rede, tenta 1 vez antes de desistir
      retry: 1,
    },
  },
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
      {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
    </QueryClientProvider>
  </React.StrictMode>
);

// Inicia monitoramento de Web Vitals após o React montar
// requestIdleCallback garante que não impacta o tempo de carregamento inicial
if (import.meta.env.PROD) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(iniciarMonitoramento);
  } else {
    setTimeout(iniciarMonitoramento, 0);
  }
}

Pipeline de CI/CD completo

O pipeline é o coração do processo de entrega contínua. Ele garante que código com problemas nunca chegue à produção e que deploys bem-sucedidos aconteçam automaticamente.

# .github/workflows/ci.yml
# Roda em Pull Requests — verifica qualidade antes do merge
name: Integração Contínua

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  # ── Qualidade e Testes ─────────────────────────────
  verificar:
    runs-on: ubuntu-latest
    name: Qualidade e Testes

    # Serviço MongoDB temporário para os testes de integração
    # O GitHub Actions sobe um container MongoDB durante o job
    services:
      mongodb:
        image: mongo:7
        ports:
          - 27017:27017

    steps:
      - uses: actions/checkout@v4

      - name: Configurar Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          # Cache de node_modules — acelera builds subsequentes
          cache: 'npm'
          cache-dependency-path: 'apps/api/package-lock.json'

      - name: Instalar dependências da API
        working-directory: apps/api
        run: npm ci

      - name: Verificar formatação (Prettier)
        working-directory: apps/api
        run: npm run format:check

      - name: Verificar lint (ESLint)
        working-directory: apps/api
        run: npm run lint

      - name: Auditoria de segurança das dependências
        working-directory: apps/api
        # Falha apenas para vulnerabilidades de severidade alta ou crítica
        run: npm audit --audit-level=high

      - name: Executar testes com cobertura
        working-directory: apps/api
        run: npm run test:coverage
        env:
          NODE_ENV: test
          MONGODB_TEST_URL: mongodb://localhost:27017/gestao-test
          JWT_SECRET: chave-de-teste-apenas-para-ci-nao-use-em-prod

      # Upload do relatório de cobertura para acompanhamento histórico
      - name: Upload de cobertura
        uses: codecov/codecov-action@v4
        if: always() # roda mesmo se os testes falharem
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          directory: apps/api/coverage

  # ── Build do Front-end ─────────────────────────────
  build-frontend:
    runs-on: ubuntu-latest
    name: Build do Front-end

    steps:
      - uses: actions/checkout@v4

      - name: Configurar Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: 'apps/web/package-lock.json'

      - name: Instalar dependências do front-end
        working-directory: apps/web
        run: npm ci

      - name: Build de produção
        working-directory: apps/web
        run: npm run build
        env:
          # URL da API de produção — usada no build
          VITE_API_URL: ${{ secrets.VITE_API_URL }}

      # Guarda os artefatos de build para o job de deploy
      - name: Salvar artefatos de build
        uses: actions/upload-artifact@v4
        with:
          name: build-web
          path: apps/web/dist
          retention-days: 1
# .github/workflows/deploy.yml
# Roda apenas em push para main — faz deploy após CI passar
name: Deploy para Produção

on:
  push:
    branches: [main]

# Garante que apenas um deploy roda por vez
# Se dois commits chegam rápido, o segundo aguarda o primeiro terminar
concurrency:
  group: deploy-producao
  cancel-in-progress: false

jobs:
  # ── Deploy da API no Railway ───────────────────────
  deploy-api:
    runs-on: ubuntu-latest
    name: Deploy API → Railway

    # Só faz deploy se o CI passou (referencia o workflow de CI)
    needs: [] # adicione o job de CI aqui se estiver no mesmo arquivo

    steps:
      - uses: actions/checkout@v4

      - name: Deploy no Railway
        uses: railwayapp/railway-action@v1
        with:
          service: ${{ secrets.RAILWAY_SERVICE_ID }}
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

      # Aguarda a API estar disponível antes de declarar sucesso
      - name: Verificar health check pós-deploy
        run: |
          echo "Aguardando API inicializar..."
          sleep 20
          # Tenta até 5 vezes com intervalo de 10 segundos
          for i in {1..5}; do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
              ${{ secrets.API_URL }}/health)
            if [ "$STATUS" = "200" ]; then
              echo "✅ API está saudável (HTTP $STATUS)"
              exit 0
            fi
            echo "⏳ Tentativa $i/5 — HTTP $STATUS. Aguardando..."
            sleep 10
          done
          echo "❌ API não respondeu após 5 tentativas"
          exit 1

  # ── Deploy do front-end na Vercel ─────────────────
  deploy-web:
    runs-on: ubuntu-latest
    name: Deploy Web → Vercel
    needs: [deploy-api] # aguarda a API estar disponível

    steps:
      - uses: actions/checkout@v4

      - name: Deploy na Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
          working-directory: apps/web

O que este projeto demonstra

Vamos olhar para o caminho percorrido — do artigo 38 ao 46 — e ver como cada peça se encaixa na aplicação final.

Módulo 6 — construiu a aplicação:
  Artigo 38 → Componentes, props, estado, JSX
  Artigo 39 → Hooks (useRef, useContext, useReducer, useMemo, hooks customizados)
  Artigo 40 → Zustand (auth, carrinho) + React Query (cache, mutations, otimismo)
  Artigo 41 → React Router (rotas, proteção, lazy loading)
  Artigo 42 → Projeto integrado: SPA completa

Módulo 7 — levou para produção:
  Artigo 43 → Deploy: Vercel + Railway + MongoDB Atlas + CI/CD básico
  Artigo 44 → Segurança: Helmet, CORS, bcrypt, JWT, rate limiting, Zod, ownership
  Artigo 45 → Performance: bundle size, memo, virtualização, índices, cache, compressão
  Artigo 46 → Integração: pipeline CI/CD completo, monitoramento, camadas coesas

Conclusão do Módulo 7

Você agora sabe não apenas construir uma aplicação, mas colocá-la no ar com qualidade profissional. Deploy automatizado que nunca envia código com testes falhando. Segurança em camadas que protege usuários e dados. Performance medida com métricas reais e otimizada com técnicas concretas.

Agora começaremos a ver padrões de projeto — as soluções elegantes que a indústria acumulou para problemas recorrentes de arquitetura de software.


 

📚 Fontes e Referências

  • Railway — Documentação: https://docs.railway.app
  • Vercel — Documentação: https://vercel.com/docs
  • GitHub Actions — Documentação: https://docs.github.com/en/actions
  • MongoDB Atlas — Documentação: https://www.mongodb.com/docs/atlas
  • web-vitals: https://github.com/GoogleChrome/web-vitals
  • Codecov: https://codecov.io
  • The DevOps Handbook — Gene Kim et al. (IT Revolution Press)
  • Accelerate — Nicole Forsgren et al. (IT Revolution Press)
  • Release It! — Michael T. Nygard (Pragmatic Bookshelf)
  • roadmap.sh — DevOps: https://roadmap.sh/devops
  • roadmap.sh — Backend: https://roadmap.sh/backend
Comentários

Mais em Javascript

Padrões de Projeto em JavaScript
Padrões de Projeto em JavaScript

Depois de anos resolvendo os mesmos tipos de problemas, a indústria de softwa...

React Hooks em profundidade
React Hooks em profundidade

No artigo anterior você aprendeu useState e useEffect — os dois hooks mais us...

Mini Projeto: Quiz Interativo
Mini Projeto: Quiz Interativo

Chegamos ao fim do M&oacute;dulo 2. Em sete artigos voc&ecirc; aprendeu a man...