kb.erickguedes.com
Node.js: Backend JavaScript em Produção

Segurança e Deploy em Produção

Aula 7 de 7

Segurança

Helmet (Headers HTTP)

const helmet = require('helmet');
const express = require('express');
const app = express();

// Configuração completa
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  frameguard: { action: 'deny' },
  hsts: { maxAge: 31536000, includeSubDomains: true }
}));

Rate Limiting

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

// Limitar por IP
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // máximo de 100 requisições
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Muitas requisições. Tente novamente mais tarde.' }
});

// Aplica globalmente
app.use(limiter);

// Limiter específico para login (mais restrito)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Muitas tentativas de login. Bloqueado por 15 minutos.' }
});

app.use('/auth/login', loginLimiter);

Proteção DOS e Slowloris

const rateLimit = require('express-rate-limit');
const slowloris = require('express-slowloris');

// Desacelerar cliente abusivo em vez de bloquear
const slowDown = rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  delayMs: (hits) => hits * 100, // delay incremental
});

app.use(slowDown);

Validação de Input

npm install zod joi
const { z } = require('zod');

const userSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['USER', 'ADMIN']).default('USER')
});

function validate(schema) {
  return (req, res, next) => {
    try {
      req.body = schema.parse(req.body);
      next();
    } catch (err) {
      return res.status(400).json({
        error: 'Dados inválidos',
        details: err.errors.map(e => ({
          field: e.path.join('.'),
          message: e.message
        }))
      });
    }
  };
}

app.post('/users', validate(userSchema), async (req, res) => {
  const user = await prisma.user.create({ data: req.body });
  res.status(201).json(user);
});

Prevenção SQL Injection

// Prisma já protege contra SQL injection
await prisma.user.findUnique({ where: { id: userId } }); // seguro

// Raw queries com Prisma (seguro se parametrizado)
await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;

// Nunca faça:
// await db.query(`SELECT * FROM users WHERE id = ${userId}`); // INJECTION!

Variáveis de Ambiente

npm install dotenv
// .env (NUNCA commitar)
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=minha-chave-secreta-super-segura
REDIS_HOST=localhost
NODE_ENV=production
LOG_LEVEL=info
// src/config.js
require('dotenv').config();

const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379
  },
  nodeEnv: process.env.NODE_ENV || 'development',
  isProduction: process.env.NODE_ENV === 'production'
};

// Validar config essencial
const required = ['DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Variável de ambiente ${key} é obrigatória`);
  }
}

module.exports = config;

Deploy

Docker

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S app && adduser -S app -u 1001

COPY --from=builder /app/node_modules ./node_modules
COPY . .

USER app
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "src/server.js"]
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file: .env
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Nginx reverse proxy

# nginx.conf
upstream node_app {
    server app:3000;
}

server {
    listen 80;
    server_name api.meudominio.com;

    gzip on;
    gzip_types application/json text/css application/javascript;

    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    location /health {
        access_log off;
        return 200 "healthy\n";
    }
}

CI/CD (GitHub Actions)

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t app .
      - name: Deploy to server
        run: |
          ssh user@server "docker pull app && docker-compose up -d"

PM2 Ecosystem

// ecosystem.config.cjs
module.exports = {
  apps: [{
    name: 'api',
    script: 'src/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    max_memory_restart: '1G',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    merge_logs: true,
    log_type: 'json',
    max_restarts: 10,
    restart_delay: 4000,
    autorestart: true,
    watch: false,
    kill_timeout: 5000,
    listen_timeout: 3000
  }]
};

Health Checks

// src/health.js
const express = require('express');
const router = express.Router();
const prisma = require('./prisma');
const redis = require('./redis');

router.get('/health', async (req, res) => {
  const checks = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  };

  // Check database
  try {
    await prisma.$queryRaw`SELECT 1`;
    checks.database = 'connected';
  } catch {
    checks.database = 'error';
    checks.status = 'degraded';
  }

  // Check Redis
  try {
    await redis.ping();
    checks.redis = 'connected';
  } catch {
    checks.redis = 'error';
    checks.status = 'degraded';
  }

  const httpStatus = checks.status === 'ok' ? 200 : 503;
  res.status(httpStatus).json(checks);
});

router.get('/ready', (req, res) => {
  // Ready check para Kubernetes/Docker
  res.status(200).json({ ready: true });
});

module.exports = router;

Lab: Exercício - Deploy Completo

# Estrutura final do projeto
meu-app/
├── src/
│   ├── server.js
│   ├── app.js
│   ├── config.js
│   ├── health.js
│   ├── routes/
│   └── middleware/
├── prisma/
│   └── schema.prisma
├── .env
├── .env.example
├── .github/
│   └── workflows/deploy.yml
├── Dockerfile
├── docker-compose.yml
├── ecosystem.config.cjs
├── nginx.conf
└── package.json
# Deploy com Docker
docker compose build
docker compose up -d

# Deploy com PM2
pm2 start ecosystem.config.cjs --env production
pm2 save
pm2 startup   # inicia com o sistema

# Verificar health
curl http://localhost:3000/health

# Logs
docker compose logs -f app
pm2 logs api

Segurança em camadas: helmet + rate-limit + validação de input + prepared statements. Sempre use .env para secrets e nunca commite. Docker + PM2 = deploy robusto com auto-healing. Health checks são essenciais para orquestração.