Configuração por ambiente no Node.js: env vars, configuração tipada e segredos

Capítulo 9

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 .env em produção para não depender de arquivo local.
  • Use .env.example para 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 zod

Crie 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 .env real. 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ávelExemploObrigatóriaDefault seguro
NODE_ENVproductionnãodevelopment
PORT3000não3000
LOG_LEVELinfonãoinfo
DATABASE_URLpostgres://...sim
JWT_SECRETstring longasim
ENABLE_REQUEST_LOGfalsenãofalse
HTTP_TIMEOUT_MS10000não10000

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.env diretamente fora de src/config.
  • Exporte apenas o objeto config (e, no máximo, tipos auxiliares).
  • Congele o objeto com Object.freeze para 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.

Agora responda o exercício sobre o conteúdo:

Qual prática melhor garante que a aplicação use configurações válidas e consistentes em todos os módulos, falhando rapidamente quando algo essencial está incorreto?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Um módulo único de configuração carrega/valida no início, aplica parsing e defaults e expõe um config tipado e congelado. Assim, a app falha rápido se faltar algo e evita acesso direto a process.env nos demais módulos.

Próximo capitúlo

Logging no Node.js e Express: logs estruturados, correlação e níveis

Arrow Right Icon
Capa do Ebook gratuito Node.js Essencial: Construindo um Back-end com Express e TypeScript
56%

Node.js Essencial: Construindo um Back-end com Express e TypeScript

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.