Voltar para Artigos
Back-end8 min de leitura

Gerenciamento Profissional de Variáveis de Ambiente (.env)

Zod para validação fail-fast no boot, coerção de tipos (PORT string para number), múltiplos arquivos .env por ambiente, dotenv nativo no Node.js 20+, e como nunca vazar secrets no git.

12 de agosto de 2026

Em 2022, a Samsung sofreu um vazamento de código-fonte porque um desenvolvedor commitou credenciais AWS por acidente. Em 2023, a Toyota expôs dados de 2 milhões de clientes por uma chave de API hardcoded em um repositório público por 5 anos. Chaves de API, secrets JWT, strings de conexão de banco — nenhum desses dados deve existir no repositório.

Além da segurança, há o problema do Fail Later: process.env.JWT_SECRET retorna undefined se a variável não estiver configurada. Sem validação, a aplicação sobe normalmente, mas lança um erro obscuro no primeiro usuário que tenta logar. A solução é Fail Fast: validar todas as variáveis requeridas no boot, antes de aceitar qualquer requisição.

dotenv Nativo no Node.js 20.6+

O padrão Fail Fast com Zod é superior ao uso direto de process.env.JWT_SECRET por três razões: 1) Tipagem — env.PORT é number, não string | undefined; 2) Validação — o processo não sobe com configuração inválida; 3) Documentação — o schema Zod documenta exatamente o que a aplicação precisa, com defaults explícitos e mensagens de erro legíveis. O .env.example comitado no repositório serve como documentação viva para novos desenvolvedores.

A partir do Node.js 20.6, o carregamento do .env é suportado nativamente — sem precisar instalar o pacote dotenv:

bash
# Node.js 20.6+: carrega .env nativamente
node --env-file=.env dist/server.js

# Para múltiplos arquivos (o último sobrescreve o anterior):
node --env-file=.env --env-file=.env.local dist/server.js

# No package.json:
# "dev": "node --env-file=.env --watch src/server.ts"

Para projetos que precisam suportar Node.js < 20.6 ou frameworks específicos:

bash
npm install dotenv
# No entry point (antes de qualquer outro import):
# import 'dotenv/config';

Validação com Zod: Fail Fast

env.ts
import { z } from 'zod';

// Zod schema para todas as variáveis de ambiente da aplicação
const envSchema = z.object({
  // === Servidor ===
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().positive().default(3333),
  // z.coerce.number(): converte '3333' (string do .env) para 3333 (number)

  // === JWT ===
  JWT_SECRET: z.string().min(32, 'JWT_SECRET deve ter ao menos 32 caracteres'),
  JWT_EXPIRES_IN: z.string().default('15m'),

  // === Banco de Dados ===
  DATABASE_URL: z.string().url('DATABASE_URL deve ser uma URL válida'),
  DATABASE_POOL_MIN: z.coerce.number().int().positive().default(2),
  DATABASE_POOL_MAX: z.coerce.number().int().positive().default(10),

  // === Redis ===
  REDIS_URL: z.string().default('redis://localhost:6379'),

  // === E-mail ===
  MAIL_PROVIDER: z.enum(['nodemailer', 'ses', 'resend']).default('nodemailer'),
  MAIL_FROM_ADDRESS: z.string().email(),
  MAIL_FROM_NAME: z.string().default('Minha Aplicação'),

  // === AWS ===
  // optional(): variável opcional — não causa erro se ausente
  AWS_REGION: z.string().optional(),
  AWS_ACCESS_KEY_ID: z.string().optional(),
  AWS_SECRET_ACCESS_KEY: z.string().optional(),

  // === Storage ===
  STORAGE_PROVIDER: z.enum(['local', 's3']).default('local'),
  UPLOAD_DIR: z.string().default('./uploads'),

  // === App ===
  FRONTEND_URL: z.string().url(),
  ALLOWED_ORIGINS: z.string().default('http://localhost:3000'),
  BCRYPT_COST: z.coerce.number().int().min(10).max(14).default(12),
});

// Fail Fast: valida no boot antes de aceitar qualquer conexão
const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error('❌ Variáveis de ambiente inválidas:');
  // Formata o erro de forma legível, listando cada campo com problema
  console.error(
    Object.entries(parsed.error.flatten().fieldErrors)
      .map(([field, errors]) => `  ${field}: ${errors?.join(', ')}`)
      .join('\n')
  );
  process.exit(1); // Mata o processo — não sobe com config inválida
}

// Exporta tipado — TypeScript sabe exatamente o tipo de cada campo
export const env = parsed.data;

// Tipos derivados do schema (útil para tipagem de outros módulos)
export type Env = z.infer<typeof envSchema>;
server.ts (entry point)
// IMPORTANTE: o env.ts deve ser o PRIMEIRO import do entry point
// Antes de qualquer outro código que use process.env
import './config/env'; // Valida e falha se inválido

import express from 'express';
import { env } from './config/env';

const app = express();

app.listen(env.PORT, () => {
  // env.PORT é number, não 'string | undefined'
  console.log(`Servidor rodando na porta ${env.PORT}`);
});

Múltiplos Arquivos por Ambiente

Estrutura de arquivos .env
.env                 # Valores padrão (comitado — sem secrets!)
.env.local           # Sobrescreve .env localmente (NÃO comitado)
.env.development     # Valores específicos de desenvolvimento (pode comitar)
.env.production      # Valores de produção (NÃO comitar — use CI/CD secrets)
.env.test            # Valores para testes (pode comitar com dados de teste)
.env.example         # Template documentado (SEMPRE comitar)
.env.example (comitado no repositório)
# === Servidor ===
NODE_ENV=development
PORT=3333

# === JWT ===
# Gere com: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=
JWT_EXPIRES_IN=15m

# === Banco de Dados ===
# PostgreSQL: postgresql://user:password@host:5432/database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dev

# === Redis ===
REDIS_URL=redis://localhost:6379

# === E-mail ===
MAIL_PROVIDER=nodemailer
MAIL_FROM_ADDRESS=noreply@exemplo.com
MAIL_FROM_NAME=Minha Aplicação

# === App ===
FRONTEND_URL=http://localhost:3000
ALLOWED_ORIGINS=http://localhost:3000

Configure o .gitignore para ignorar .env, .env.local, .env.production e qualquer arquivo .env.* que não seja .env.example. Adicione também um git pre-commit hook com git-secrets ou detect-secrets para bloquear commits que contenham padrões de secrets (chaves AWS, tokens JWT).

Rotação e Auditoria de Secrets

  • Nunca hardcode secrets no código — mesmo em branches temporárias. Use environment variables.
  • Rotacione regularmente secrets de produção (JWT_SECRET, chaves de API). Com o padrão de Refresh Token Rotation, trocar o JWT_SECRET invalida todas as sessões gradualmente.
  • Use um Secret Manager em produção: AWS Secrets Manager, HashiCorp Vault ou GCP Secret Manager. Os secrets são injetados como variáveis de ambiente pelo runtime, nunca ficam em arquivos.
  • Audit trail: toda mudança de secret em produção deve ser registrada com quem fez e quando.
  • Revogue imediatamente qualquer secret que tenha sido exposto — mesmo por segundos em um commit ou log.

Conclusão

Validação com Zod no boot transforma um problema de runtime (erro misterioso na primeira requisição) em um problema de startup (o processo não sobe, mensagem clara). O .env.example documenta o que a aplicação precisa. E o Secret Manager em produção garante que nem os devops precisam saber os valores reais dos secrets — o sistema os injeta automaticamente.