• Início
  • Blog
  • Um Guia Prático de Configuração em Camadas para Aplicações JavaScript Modernas

Um Guia Prático de Configuração em Camadas para Aplicações JavaScript Modernas

Um passo a passo para organizar, validar e escalar a configuração da sua aplicação sem dor de cabeça.

Publicado em 01 de jul. de 2025, e leva aproximadamente 5 minutos para ler.

Escrever aplicações que escalam é difícil.

Para conseguir isso, na maioria das vezes tentamos aplicar padrões e práticas de projetos bem conhecidas para lidar melhor com os problemas que encontramos nesse processo.

Um problema que toda aplicação já escrita enfrenta é a gestão das configurações da aplicação. Pode ser algo básico como a port que seu app deve usar, mas pode ir mais fundo, como o host do banco de dados, URLs de APIs, configurações específicas de funcionalidades, etc.

Neste post, quero falar um pouco sobre esse problema e propor uma solução para quando você estiver escrevendo aplicações JS/TS que com certeza vai te ajudar a organizar melhor as configs da sua app de uma melhor forma.

O Problema da Configuração

Digamos que você está construindo uma app e ela precisa de 2 configurações iniciais: a porta e as informações para acessar do banco de dados.

Você poderia declarar isso no arquivo onde está sendo usada, mas como boa prática, você pode isolar-las em um arquivo:

src/config.ts
const config = {
  port: 3000,
  db: {
    host: "localhost",
    port: 5432,
  },
} as const;

Legal. Agora, todo mundo que trabalhar nessa applicação, pode deduzir que as configurações do projeto vão estar todas centralizadas nesse arquivo.

Mas agora temos um problema: enquanto isso funciona localmente, queremos fazer o deploy da nossa aplicação e usar configurações diferentes. Por enquanto, vamos supor que teremos apenas um ambiente "local" e "produção".

Para adequar à esse novo requisito, vamos expandir nosso config.ts e considerar o uso de uma variável de ambiente (APP_ENV) para determinar o ambiente que a nossa aplicação está rodando:

src/config.ts
interface AppConfig {
  port: number;
  db: {
    host: string;
    port: number;
  };
}

type Environment = "local" | "prod";

const localConfig: AppConfig = {
  port: 3000,
  db: {
    host: "localhost",
    port: 5432,
  },
};

const productionConfig: AppConfig = {
  port: 8080,
  db: {
    host: "db.example.com",
    port: 5432,
  },
};

const configMap: Record<Environment, AppConfig> = {
  prod: productionConfig,
  local: localConfig,
};

// @ts-expect-error - A gente sabe que pode ser undefined
export const config = configMap[process.env.APP_ENV];

if (!config) {
  throw new Error(
    `Configuration for environment "${process.env.APP_ENV}" not found.`,
  );
}

Até aqui, o código ficou um pouco mais complexo, mas ainda é gerenciável. Isso porque só temos dois ambientes e nossa config é pequena, mas e se quisermos ter mais ambientes como dev e/ou staging?

Bem, precisaríamos adicionar esses valores ao nosso tipo Environment e implementar a configuração.

Para organizar melhor o código, você poderia até separar em arquivos, assim:

./
└─ ...
└─ src/
   └─ ...
   └─ config/
      ├─ dev.ts
      ├─ index.ts
      ├─ local.ts
      ├─ prod.ts
      ├─ staging.ts
      └─ type.ts

Em cada arquivo de configuração, você declara os valores para aquele ambiente e no final, teremos o nosso config.ts assim:

src/config.ts
import { devConfig } from "./dev";
import { localConfig } from "./local";
import { productionConfig } from "./prod";
import { stagingConfig } from "./staging";
import type { AppConfig, Environment } from "./types";

const configMap: Record<Environment, AppConfig> = {
  prod: productionConfig,
  local: localConfig,
  dev: devConfig,
  staging: stagingConfig,
};

// @ts-expect-error - We know it can be undefined
export const config = configMap[process.env.APP_ENV];

if (!config) {
  throw new Error(
    `Configuration for environment "${process.env.APP_ENV}" not found.`,
  );
}

Bem melhor de entender e organizar, mas ainda temos algumas falhas nesse fluxo, por exemplo:

... E se, para todos os ambientes que temos, a configuração for a mesma? Ou, for igual para todos, exceto para produção?

Aqui, você poderia ter uma "config base" que vai ser usada para compor a configuração do outro ambiente (se tiver objetos aninhados, precisa fazer um deep merge):

// config/base.ts
import type { AppConfig } from "./types";

export const baseConfig: AppConfig = {
  port: 3000,
  db: {
    host: "localhost",
    port: 5432,
  },
};

// config/dev.ts
import { baseConfig } from "./base";
import type { AppConfig } from "./types";

export const devConfig: AppConfig = deepMerge({}, baseConfig, {
  port: 8080,
  db: {
    host: "db-dev.example.com",
  },
});

Isso vai gerar um objeto de config assim:

{
  "port": 8080,
  "db": {
    "host": "db-dev.example.com",
    "port": 5432, // essa porta vai vir do base
  },
}

... E se eu quiser validação em tempo de execução a minha configuração?

Nesse caso, você pode usar o zod ou qualquer biblioteca de validação, definir o schema da sua config e validar-la. Assim, você garante segurança de tipos tanto em tempo de compilação quanto em tempo de execução:

// config/types.ts
import { z } from "zod/v4";

export const AppConfig = z.object({
  port: z.number(),
  db: z.object({
    host: z.string(),
    port: z.number(),
  }),
});
export type AppConfig = z.Infer<typeof AppConfig>;

export const Environment = z.enum(["dev", "local", "prod", "staging"]);
export type Environment = z.Infer<typeof Environment>;

// config/index.ts
import { devConfig } from "./dev";
import { localConfig } from "./local";
import { productionConfig } from "./prod";
import { stagingConfig } from "./staging";
import { AppConfig, Environment } from "./types";

const configMap: Record<Environment, AppConfig> = {
  prod: productionConfig,
  local: localConfig,
  dev: devConfig,
  staging: stagingConfig,
};

const env = Environment.parse(process.env.APP_ENV);
export const config = AppConfig.parse(configMap[env]);

... E se eu quiser usar variáveis de ambiente?

Para isso, provavelmente você teria que fazer algo assim:

config/local.ts
import { z } from "zod/v4";
import type { AppConfig } from "./types";

const port = z.coerce.number().default(3000).parse(process.env.PORT);

export const localConfig: AppConfig = {
  port,
  db: {
    host: "localhost",
    port: 5432,
  },
};

... E se eu não quiser lidar com tudo isso?

Começa a ficar trabalhoso, né? Mas antes de resolvermos isso com uma solução melhor, quero falar rapidamente sobre uma boa estratégia que outras linguagens promovem chamada Configuração em Camadas (Layered Configuration).

Configuração em Camadas e 12Factor App

Configuração em Camadas (Layered Configuration em inglês) é um conceito abstrato para gerenciar configurações de aplicações ou sistemas organizando-as como camadas independentes que são combinadas para produzir uma configuração final.

No nosso contexto, é um pouco do que já discutimos: ter arquivos separados para cada ambiente para facilitar o gerenciamento.

Além disso, podemos trazer também o 12Factor App, que é um conjunto de 12 princípios para construir aplicações melhores e mais escaláveis.

O terceiro conceito é chamado "Config" e define que devemos mover as configurações para fora do nosso código. Ele defende o uso de configuração apenas como variáveis de ambiente:

Pessoalmente, não sou muito fã dessa ideia. Digo, variáveis de ambiente são legais, mas prefiro ter a configuração (quando não sensível) armazenada no repositório. Assim, conseguimos rastrear facilmente as mudanças e reverter se necessário.

Mas também gosto de poder sobrescrever esses valores via variável de ambiente se eu precisar, por exemplo, rodar minha aplicação "como produção" mas talvez só mudar a URL da API, para rodar o app localmente.

Nesse sentido, podemos misturar Configuração em Camadas e a estratégia de Config do 12Factor-app em uma nova abordagem.

Layerfig

Para resolver esse problema e trazer uma solução que implemente esses conceitos, criei o Layerfig.

Ela foi fortemente inspirada pela biblioteca oficial do rust para lidar com esse problema: config-rs.

Veja como funciona:

1. Você cria seus arquivos de configuração:

./
├─ config/
│  ├─ base.json
│  ├─ local.json
│  └─ prod.json
└─ src

2. Instale a biblioteca

npm install @layerfig/config

3. Cria seu arquivo de config que vai adicionar os recursos e exportar o objeto de configuração:

import { ConfigBuilder, z } from "@layerfig/config";
import { FileSource } from "@layerfig/config/sources/file";
import { EnvironmentVariableSource } from "@layerfig/config/sources/env";

const Environment = z.enum(["local", "prod"]);
const env = Environment.parse(process.env.APP_ENV);

const configSchema = z.object({
  appURL: z.url(),
});

export const config = new ConfigBuilder({
  validate: (finalConfig) => {
    return configSchema.parse(finalConfig);
  },
})
  .addSource(new FileSource("base.json"))
  .addSource(new FileSource(`${env}.json`))
  .addSource(new EnvironmentVariableSource())
  .build();

... e é isso.

As fontes que você adiciona serão carregadas e mescladas na ordem que você definiu:

  1. carrega do base.json
  2. carrega do <env>.json e sobrescreve o base
  3. carrega das variáveis de ambiente e sobrescreve os dois objetos anteriores

Agora, quando você importar seu objeto config na sua aplicação, esse objeto será seguro em tipos tanto em tempo de compilação quanto em tempo de execução.

Principais recursos

  • É focada no lado do servidor, mas também é possível utilizar no lado do client através do ObjectSource.
  • Pode ser usado em Node, Bun, Deno e qualquer engine JS que suporte node:fs e node:path.
  • Você pode usar o zod/v4 embutido e exportado ou usar sua própria biblioteca de validação. Tudo que precisa fazer é retornar o resultado da validação do schema na função validate.
  • Por padrão, apenas arquivos .json são suportados, mas se quiser .jsonc, .json5, .yml ou .toml, pode usar os parsers oficiais.
  • Você pode usar slots e definir variáveis de ambiente dentro da config.
  • Permite sobrescrever valores (inclusive propriedades aninhadas e profundas) via variáveis de ambiente.

Veja a documentação para mais detalhes e exemplos.

Conclusão

Espero ter conseguido mostrar como podemos gerenciar melhor as configurações da aplicação e como o layerfig pode te ajudar nesse caso.

Se tiver qualquer problema ao usar ou quiser alguma funcionalidade específica, fique à vontade para participar no repositório do layerfig no github.

Referências