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.
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:
# 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:
npm install dotenv
# No entry point (antes de qualquer outro import):
# import 'dotenv/config';Validação com Zod: Fail Fast
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>;// 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
.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)# === 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:3000Configure 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.