O que é configuração por ambiente (e por que isso importa)
Configuração por ambiente é a prática de ajustar o comportamento da aplicação conforme o contexto em que ela roda (desenvolvimento, teste, produção) sem alterar o código-fonte. Em Node.js, isso normalmente é feito com variáveis de ambiente (env vars) e, quando apropriado, arquivos de configuração locais (especialmente para desenvolvimento e testes). A ideia central é: o mesmo build deve poder rodar em ambientes diferentes apenas mudando a configuração.
Boas configurações por ambiente resolvem problemas comuns: apontar para bancos diferentes, ajustar nível de log, habilitar/desabilitar features, definir timeouts, configurar CORS, portas, URLs externas e, principalmente, lidar com segredos (tokens, senhas, chaves).
Princípios práticos
- Fail fast: valide a configuração na inicialização e falhe imediatamente se algo essencial estiver ausente ou inválido.
- Config tipada: exponha um objeto de configuração com tipos claros para o resto da aplicação.
- Defaults seguros: valores padrão devem ser conservadores (ex.: não habilitar debug em produção).
- Segredos fora do código: nunca comitar credenciais; preferir env vars/secret managers.
- Consistência: um único módulo de configuração consumido por toda a aplicação.
Estrutura recomendada de pastas e arquivos
Uma estrutura simples e eficaz:
src/ config/ env.ts # carrega .env (apenas dev/test) e expõe process.env schema.ts # validação e parsing (fail fast) index.ts # exporta config tipado único server.ts ...Arquivos .env ficam na raiz do projeto (fora de src) e não devem ser versionados (exceto um exemplo):
.env # local (NÃO versionar) .env.test # para testes locais (NÃO versionar) .env.example # exemplo versionado (SEM segredos)Passo a passo: carregando env vars com segurança
1) Defina quais ambientes existem
Padronize um conjunto pequeno de ambientes. Um padrão comum é usar NODE_ENV com valores: development, test, production.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
2) Carregue arquivos .env apenas quando fizer sentido
Em produção, é comum receber env vars do sistema/infra (container, VM, orquestrador). Já em desenvolvimento e teste local, um .env facilita.
Crie src/config/env.ts:
import path from "node:path";import fs from "node:fs";import dotenv from "dotenv";const nodeEnv = process.env.NODE_ENV ?? "development";const isProd = nodeEnv === "production";if (!isProd) { // Carrega .env e/ou .env.test conforme NODE_ENV const envFile = nodeEnv === "test" ? ".env.test" : ".env"; const envPath = path.resolve(process.cwd(), envFile); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); }}export const rawEnv = process.env;Observações:
- Evite carregar
.envem produção para não depender de arquivo local. - Use
.env.examplepara documentar variáveis necessárias sem incluir segredos.
Passo a passo: validação (fail fast) e configuração tipada
O objetivo é transformar process.env (string e opcional) em um objeto tipado, com parsing e validação. Assim, o restante da aplicação não precisa lidar com undefined nem converter strings manualmente.
3) Crie um schema de validação e parsing
Uma abordagem prática é usar um validador de schema (por exemplo, Zod) para:
- validar presença/formato
- aplicar defaults
- converter tipos (string → number/boolean)
Instalação:
npm i zodCrie src/config/schema.ts:
import { z } from "zod";const toBool = (v: string) => ["1", "true", "yes", "on"].includes(v.toLowerCase());export const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]).default("development"), PORT: z.coerce.number().int().min(1).max(65535).default(3000), LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"), DATABASE_URL: z.string().url(), // Segredos (exemplos) JWT_SECRET: z.string().min(32), // Flags ENABLE_REQUEST_LOG: z .string() .optional() .transform((v) => (v ? toBool(v) : false)), // Exemplo de timeout em ms HTTP_TIMEOUT_MS: z.coerce.number().int().min(100).max(120000).default(10000),});export type Env = z.infer<typeof envSchema>;Pontos importantes:
z.coerce.number()converte string para número e valida.- Defaults são aplicados com
.default(...). - Segredos têm validação mínima (ex.: tamanho) para evitar valores fracos.
4) Faça o parse na inicialização e falhe com mensagem útil
Crie src/config/index.ts para ser o módulo único de configuração:
import "./env"; // garante que .env (dev/test) foi carregado antes do parseimport { envSchema, type Env } from "./schema";function formatZodError(err: unknown): string { // Mantém mensagem simples e útil sem vazar valores if (err && typeof err === "object" && "issues" in err) { const issues = (err as any).issues as Array<{ path: (string | number)[]; message: string }>; return issues .map((i) => `${i.path.join(".")}: ${i.message}`) .join("; "); } return "Invalid environment configuration";}let parsed: Env;try { parsed = envSchema.parse(process.env);} catch (e) { const msg = formatZodError(e); // Fail fast: encerra o processo na inicialização // Não logue process.env aqui console.error(`[config] ${msg}`); process.exit(1);}export const config = Object.freeze({ env: parsed.NODE_ENV, isProd: parsed.NODE_ENV === "production", isTest: parsed.NODE_ENV === "test", http: { port: parsed.PORT, timeoutMs: parsed.HTTP_TIMEOUT_MS, }, log: { level: parsed.LOG_LEVEL, enableRequestLog: parsed.ENABLE_REQUEST_LOG, }, db: { url: parsed.DATABASE_URL, }, auth: { jwtSecret: parsed.JWT_SECRET, },});Agora, qualquer parte da aplicação importa config e recebe dados consistentes e tipados:
import { config } from "./config";console.log(config.http.port);5) Garanta que o módulo de config seja carregado cedo
Em src/server.ts (ou no entrypoint), importe config antes de iniciar servidor, conectar no banco etc. Isso garante o fail fast antes de qualquer efeito colateral.
import { config } from "./config";import express from "express";const app = express();app.listen(config.http.port, () => { // Evite logar segredos console.log(`[server] listening on port ${config.http.port} (${config.env})`);});Quando usar arquivos de configuração além de env vars
Variáveis de ambiente são ótimas para valores pequenos e sensíveis. Para configurações maiores (listas, mapas, regras), pode ser mais confortável usar arquivos (JSON/YAML/TS) sem segredos, combinados com env vars para itens sensíveis.
Exemplo: config por ambiente sem segredos em arquivo
Crie src/config/app-config.ts:
type PublicAppConfig = { features: { enableDocs: boolean; }; cors: { allowedOrigins: string[]; };};const base: PublicAppConfig = { features: { enableDocs: false }, cors: { allowedOrigins: [] },};const byEnv: Record<"development" | "test" | "production", Partial<PublicAppConfig>> = { development: { features: { enableDocs: true }, cors: { allowedOrigins: ["http://localhost:5173"] }, }, test: { features: { enableDocs: false }, }, production: { features: { enableDocs: false }, },};export function getPublicAppConfig(env: "development" | "test" | "production"): PublicAppConfig { return { ...base, ...byEnv[env], features: { ...base.features, ...byEnv[env].features }, cors: { ...base.cors, ...byEnv[env].cors }, };}E então componha no módulo único:
import { getPublicAppConfig } from "./app-config"; // ... dentro de src/config/index.tsexport const config = Object.freeze({ // ... app: getPublicAppConfig(parsed.NODE_ENV),});Regra prática: segredos ficam em env vars; arquivos versionados carregam apenas configuração pública.
Separação de segredos: práticas essenciais
O que é segredo (e o que não é)
Considere segredo qualquer dado que permita acesso indevido: senhas, tokens, chaves privadas, connection strings com credenciais, API keys, cookies signing secrets, JWT secrets.
Não são segredos (em geral): porta, nível de log, flags de feature, timeouts, nomes de serviços, URLs públicas sem credenciais.
Boas práticas para não vazar credenciais
- Nunca versionar
.envreal. Versione apenas.env.example. - Não logar objetos de configuração completos nem
process.env. - Redação (redaction) em logs: se precisar logar algo relacionado, mascare valores.
- Erros de validação devem mencionar o nome da variável, não o valor.
- Princípio do menor privilégio: credenciais com permissões mínimas necessárias.
Exemplo: função utilitária para mascarar segredos em logs
export function redact(value: string, visibleStart = 3, visibleEnd = 2): string { if (!value) return ""; if (value.length <= visibleStart + visibleEnd) return "***"; const start = value.slice(0, visibleStart); const end = value.slice(-visibleEnd); return `${start}***${end}`;}Uso (apenas se realmente necessário):
import { config } from "./config";import { redact } from "./utils/redact";console.log(`[auth] jwt secret loaded: ${redact(config.auth.jwtSecret)}`);Em geral, prefira não logar segredos nem mascarados; mas a redação ajuda em diagnósticos controlados.
Rotação de segredos e impacto no design
Rotação é trocar segredos periodicamente ou após incidente. Para suportar rotação com menos atrito:
- Não embuta segredos no build: use env vars/secret store para trocar sem rebuild.
- Planeje múltiplas chaves quando necessário (ex.: JWT): aceite chave atual e anterior por um período.
- Reinicialização controlada: se o segredo muda via env var, normalmente exige restart do processo para recarregar (a menos que você implemente reload).
Exemplo: suportando duas chaves de JWT (atual e anterior)
No schema:
JWT_SECRET: z.string().min(32),JWT_SECRET_PREVIOUS: z.string().min(32).optional(),No config:
auth: { jwtSecrets: [parsed.JWT_SECRET, parsed.JWT_SECRET_PREVIOUS].filter(Boolean) as string[],},Na verificação de token, tente validar com a chave atual e, se falhar, com a anterior (a implementação depende da biblioteca de JWT utilizada).
Checklist de variáveis e defaults seguros
| Variável | Exemplo | Obrigatória | Default seguro |
|---|---|---|---|
| NODE_ENV | production | não | development |
| PORT | 3000 | não | 3000 |
| LOG_LEVEL | info | não | info |
| DATABASE_URL | postgres://... | sim | — |
| JWT_SECRET | string longa | sim | — |
| ENABLE_REQUEST_LOG | false | não | false |
| HTTP_TIMEOUT_MS | 10000 | não | 10000 |
Padronizando o consumo: um único módulo de configuração
O módulo src/config/index.ts deve ser a única fonte de verdade. Para reforçar isso:
- Evite acessar
process.envdiretamente fora desrc/config. - Exporte apenas o objeto
config(e, no máximo, tipos auxiliares). - Congele o objeto com
Object.freezepara evitar mutações acidentais. - Organize por domínios (
http,db,auth,log) para facilitar descoberta e consistência.
Exemplo de uso consistente em diferentes módulos
// src/db/client.tsimport { config } from "../config";export function connectDb() { // use config.db.url return config.db.url;}// src/log/logger.tsimport { config } from "../config";export const loggerConfig = { level: config.log.level };// src/auth/jwt.tsimport { config } from "../config";export const jwtSecret = config.auth.jwtSecret;Com isso, a aplicação ganha previsibilidade: se o processo iniciou, a configuração é válida, tipada e coerente em todos os pontos.