NestJS: lendo as variáveis de ambiente com o ConfigService

Aprenda a usar o ConfigModule e o ConfigService do framework NestJS para organizar as variáveis de configuração da sua aplicação, incluindo as variáveis de ambiente. Descubra como gerenciar configurações para diferentes ambientes e trazer mais flexibilidade ao seu projeto.

NestJS: lendo as variáveis de ambiente com o ConfigService
NestJS: lendo variáveis de ambiente com o ConfigService

Para quem não conhece, NestJS é um framework para a construção de aplicações backend que rodam com Node.js. O TypeScript é a linguagem principal, e sua sintaxe se parece com a do framework Angular. Particularmente, também acho um pouco parecido com o Spring Boot do Java, mas é uma opinião totalmente pessoal.

Breve introdução às variáveis de ambiente

Independente da linguagem de programação e framework escolhido, é comum que nossas aplicações rodem em diferentes ambientes, como no seu computador durante o desenvolvimento, em um ambiente de teste ou em produção. Cada ambiente pode ter suas particularidades. Por exemplo, a string de conexão com o banco de dados no seu computador é diferente daquela usada no servidor de produção.

Para lidar com essas diferenças, usamos variáveis de ambiente, que são basicamente pares de chave e valor ajustados de acordo com o ambiente. Essas variáveis são configuradas de maneira específica para cada sistema operacional e linguagem, e são definidas antes da execução da aplicação. Uma vez estabelecidas, a aplicação pode acessá-las durante sua execução para configurar parâmetros, como a string de conexão com o banco de dados, endereços de APIs externas, configuração de log e outros valores que podem variar conforme o ambiente onde a aplicação está rodando.

Para mostrar este cenário na prática, será criado a seguir um projeto utilizando NestJS. Em seguida, será apresentado como utilizar o ConfigModule e ConfigService para criar um objeto de configuração, onde parte dos valores podem ser obtidos através da leitura de variáveis de ambiente.

Criando o projeto de estudo

Primeiramente vamos criar um projeto do zero para ser o cenário de estudo:

# Conferindo a versão do Node.js:
node --version
# v20.18.0

# Instalando o CLI do NestJS de forma global:
npm i -g @nestjs/cli

# Utilizei a versão 10.4.5 do NestJS. Para verificar
# use o comando `nest --version`
# Criando o projeto:
nest new usando-config-service

# Após executar o comando acima, o CLI irá 
# perguntar qual é sua escolha para gerenciador
# de pacotes. Neste exemplo eu selecionei o NPM.

# Após criado, basta acessar o diretório do projeto...
cd usando-config-service

# ...e executar:
npm run start:dev

Após executar os comandos acima, a aplicação será executada na porta padrão, 3000. Através do navegador ou qualquer outro cliente TCP, podemos efetuar um teste consultando o endereço http://localhost:3000.

# Exemplo utilizando o curl
curl http://localhost:3000

O resultado obtido será Hello World!.

Instalando o pacote nestjs/config

Agora que o projeto está criado, vamos instalar a dependência @nestjs/config com o comando:

npm install @nestjs/config
💡
Internamente o pacote nestjs/config utiliza o pacote dotenv.
https://docs.nestjs.com/techniques/configuration#installation

Configurando o ConfigModule no NestJS

Com o pacote @nestjs/config instalado, precisamos configurar o ConfigModule na aplicação. Ele será responsável por carregar as variáveis de ambiente do arquivo .env (será criado a seguir) e torná-las acessíveis através do ConfigService.

Importando e configurando o ConfigModule no AppModule:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      // O parâmetro isGlobal: true garante que o
      // ConfigService esteja disponível em toda a
      // aplicação, sem a necessidade de importá-lo
      // manualmente em cada módulo.
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.module.ts

Feito a alteração acima, foi criado o arquivo.env na raiz do projeto com o seguinte conteúdo:

VALOR1=AAA
VALOR2=123
VALOR3=true
VALOR4=2024-10-21T12:00:00.000Z

.env

Usando o ConfigService para ler as variáveis de ambiente

Com o ConfigModule configurado, podemos injetar o ConfigService nas classes que desejarmos, por exemplo, vamos alterar o AppController para ler algumas variáveis de ambiente e entregá-las na resposta da requisição:

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Controller()
export class AppController {
  // Basta declarar o ConfigService que o NestJS irá
  // injetar o objeto.
  constructor(private configService: ConfigService) {}

  @Get()
  getHello() {
    const valor1 = this.configService
      .get<string>('VALOR1');

    const valor2 = this.configService
      .get<number>('VALOR2');

    const valor3 = this.configService
      .get<boolean>('VALOR3');

    const valor4 = this.configService
      .get<Date>('VALOR4');

    return { valor1, valor2, valor3, valor4 };
  }
}

app.controller.ts

Ao executarmos o projeto e acessarmos o endereço localhost:3000, a resposta será a exatamente os valores lidos das variáveis de ambiente declaradas no arquivo .env:

{
  "valor1": "AAA",
  "valor2": "123",
  "valor3": "true",
  "valor4": "2024-10-21T12:00:00.000Z"
}

Aqui vale destacar algumas observações:

  • o ConfigService permite a leitura das variáveis de ambiente de forma similar ao process.env.VARIAVEL, então qual a vantagem?
  • perceba que por padrão, as variáveis de ambiente são simplesmente um conjunto de chaves e valores do tipo string, são textos. Observe que mesmo adicionando a tipagem no método get<>, não acontece uma conversão de tipo. Como tratar este cenário e obter os dados com a tipagem desejada?

Para responder as duas dúvidas, vamos melhorar a estrutura criando um arquivo de configuração, como se fosse uma camada entre as variáveis de ambiente e o ConfigService.

Como criar configurações tipadas com o ConfigModule no NestJS

O NestJS possibilita a criação de um arquivo de configuração para organizar melhor as variáveis que nossa aplicação irá utilizar, dentre essas variáveis também podemos incluir as de ambiente.

For more complex projects, you may utilize custom configuration files to return nested configuration objects. This allows you to group related configuration settings by function (e.g., database-related settings), and to store related settings in individual files to help manage them independently.

https://docs.nestjs.com/techniques/configuration#custom-configuration-files

Para isto, podemos criar um arquivo chamado configuration.ts, o nome do arquivo é opcional, e adicionar o seguinte conteúdo:

export default () => ({
  // A estrutura do objeto criado abaixo é
  // totalmente arbitrária.
  minhaAplicacao: {
    valor1: process.env.VALOR1,
    valor2: parseInt(process.env.VALOR2, 10),
    valor3: process.env.VALOR3 === 'true',
    valor4: new Date(process.env.VALOR4),
    qualquerOutroValor: 12345
  },
  /*
  bancoDeDados: {
  
  },
  apiUsuarios: {
    baseUrl: '',
    endpoint1: '',
  },
  /*
});

configuration.ts

Perceba que podemos declarar absolutamente qualquer coisa neste arquivo, é basicamente um objeto com valores para configuração da aplicação. Também podemos aproveitar e efetuar a conversão das variáveis de ambiente para o tipo que esperamos, por exemplo, parseInt(process.env.VALOR2, 10).

Por fim, passamos o conteúdo deste arquivo como referência no ConfigModule na propriedade load:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configuration from './configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      // O parâmetro isGlobal: true garante que o
      // ConfigService esteja disponível em toda a
      // aplicação, sem a necessidade de importá-lo
      // manualmente em cada módulo.
      isGlobal: true,
      load: [configuration],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Feito isto, podemos utilizar o método get do ConfigService passando como parâmetro da consulta a estrutura criada no arquivo de configuração, veja a seguir:

import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Controller()
export class AppController {
  constructor(private configService: ConfigService) {}

  @Get()
  getHello() {
    const valor1 = this.configService.get<string>(
      'minhaAplicacao.valor1',
    );

    const valor2 = this.configService.get<number>(
      'minhaAplicacao.valor2',
    );

    const valor3 = this.configService.get<boolean>(
      'minhaAplicacao.valor3',
    );

    const valor4 = this.configService.get<Date>(
      'minhaAplicacao.valor4',
    );

    // ************************************************
    // Exemplo: definimos um valor default caso o `get`
    // não encontre um valor para
    // 'minhaAplicacao.valor_inexistente'
    // ************************************************
    const valor5 = this.configService.get<number>(
      'minhaAplicacao.valor_inexistente',
      5 /* valor default */,
    );

    console.log(typeof valor1 === 'string');  // true
    console.log(typeof valor2 === 'number');  // true
    console.log(typeof valor3 === 'boolean'); // true
    console.log(valor4 instanceof Date);      // true
    console.log(typeof valor5 === 'number');  // true

    return { valor1, valor2, valor3, valor4, valor5 };
  }
}

app.controller.ts

Observe no resultado abaixo que valor2 é um número, valor3 é um booleano, e valor4 é uma data. Vale lembrar que, no JSON, objetos Date são representados como strings no formato ISO-8601. Já o valor5 é o número 5, que corresponde ao valor padrão caso a chave minhaAplicacao.valor_inexistente não seja encontrada nas configurações.

{
  "valor1": "AAA",
  "valor2": 123,
  "valor3": true,
  "valor4": "2024-10-21T12:00:00.000Z",
  "valor5": 5
}

Assim melhoramos a organização das variáveis que nossa aplicação irá utilizar, além de obtermos estes dados já em sua tipagem correta. Essa é uma das vantagens do ConfigService em relação ao process.env.

Como trabalhar com vários arquivos de configuração .env

Eventualmente teremos em nosso ambiente de desenvolvimento mais de um arquivo .env, por exemplo, development.env e local.env. Estes nomes não são regras, são apenas exemplos. Uma das formas de chavearmos entre um arquivo e outro é utilizando a propriedade envFilePath do ConfigModule. Essa propriedade permite a declaração de uma lista de arquivos. O ConfigModule irá ler da esquerda para direita mantendo como prioridade o que vem primeiro. Por exemplo, observe os dois arquivos .env a seguir:

VALOR1=AAA
VALOR3=true
VALOR4=2024-10-21T12:00:00.000Z

local.env

VALOR1=BBB
VALOR2=111111123
VALOR3=false
VALOR4=2024-12-21T12:00:00.000Z

development.env

Declarando os dois arquivos .env no ConfigModule:

@Module({
  imports: [
    ConfigModule.forRoot({
      // ...configurações utilizadas anteriormente...
      envFilePath: ['local.env', 'development.env'],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.module.ts

Ao executar o projeto, o seguinte resultado será obtido ao consultar o endereço localhost:3000:

{
  "valor1": "AAA",
  "valor2": 111111123,
  "valor3": true,
  "valor4": "2024-10-21T12:00:00.000Z",
  "valor5": 5
}

Perceba que valor2 não existe no arquivo local.env, mas existe no development.env. Então o ConfigService manteve o valor do segundo arquivo visto que ele não foi encontrado no primeiro. Já valor1, valor3 e valor4 foram lidos do arquivo local.env, que foi declarado antes do development.dev. É crucial entender essa regra de precedência.

Escolhendo qual arquivo utilizar

Uma forma simples de definir qual arquivo você vai utilizar é através de variáveis de ambiente. Podemos preencher uma variável de ambiente, por exemplo NODE_ENV, com algum valor na declaração do script dentro do package.json e usar este valor para definir qual arquivo será carregado:

{
  "...": "...outras propriedades",
  "scripts": {
    "start:dev": "NODE_ENV=development nest start --watch",
    "start:local": "NODE_ENV=local nest start --watch",
  },
}

trecho do package.json

Observação: caso esteja executando a aplicação no CMD do Windows, utilize o seguinte:

{
  "...": "...outras propriedades",
  "scripts": {
    "start:dev": "set NODE_ENV=development && nest start --watch",
    "start:local": "set NODE_ENV=local && nest start --watch",
  },
}

trecho do package.json

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configuration from './configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      // O parâmetro isGlobal: true garante que o
      // ConfigService esteja disponível em toda a
      // aplicação, sem a necessidade de importá-lo
      // manualmente em cada módulo.
      isGlobal: true,
      load: [configuration],
      envFilePath: [`${process.env.NODE_ENV || ''}.env`],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.module.ts

Ao executar a aplicação com o comando npm run start:local, o arquivo local.env será utilizado. Já ao executar npm run start:dev, o arquivo development.env será carregado. Caso a variável NODE_ENV não esteja definida, o arquivo .env será usado como padrão.

💡
Cuidado com o arquivo .env! É recomendável a não inclusão do arquivo .env no seu repositório. Incluir o arquivo.env no repositório pode levar a vazamentos de dados e comprometer sua aplicação. Como alternativa podemos destacar a utilização de um serviço especializado como o Azure Key Vault ou a criação manual do arquivo .env dentro dos seus servidores.

Considerações

ConfigService do NestJS facilita o gerenciamento de variáveis de configuração da aplicação, dentre elas, as variáveis de ambiente. Com ele, podemos centralizar todas as configurações da aplicação, garantindo que nossa aplicação se comporte de forma consistente em diferentes ambientes.

Abaixo deixo o link da documentação oficial: