leaf-logo

k1nha

Uso de Variáveis de Ambiente em Aplicações Node e Nest

02/02/2024

E aí 🚀

Quando estamos construindo aplicações backend, é crucial lidar com informações sensíveis, como URLs para conexão com o banco de dados, chaves secretas e outras variáveis críticas.

E como não queremos deixar essas informações expostas, recorremos ao uso variáveis de ambiente para proteger nossa aplicação e evitar exposições indesejadas.

Nesse artigo, apresentarei uma forma que tenho usado em minhas aplicações Node e Nest.

Passo 1: Instalação dos pacotes - Node

Para projetos node, vamos precisar dos seguintes pacotes, zod e dotenv:

npm install zod dotenv

Dependendo da sua entrutura de pastas, costumo criar uma pasta para config e, dentro dela, um arquivo chamado env.ts.

Passo 2: Criação do schema

O que vamos fazer agora é utilizar o zod para validação de nossas variáveis de ambiente.

import 'dotenv/config'
import { z } from 'zod'

const environmentSchema = z.object({
  NODE_ENV: z.enum(['dev', 'test', 'prod']).default('dev'),
  PORT: z.coerce.number().default(3333),
  DB_URL: z.string(),
})

Anteriormente, importamos o zod e dotenv e, em seguida, criamos um esquema (schema) com base nas variaveis que serão utilizadas em nossa aplicação. Nesse caso,estamos considerando apenas as variáveis PORT/ DB_URL e NODE_ENV.

Mas como validamos esse esquema para nossas variaveis de ambiente?

// Código acima
const _env = environmentSchema.safeParse(process.env) //

if (!_env.success) {
  console.error('Invalid environment variables', _env.error.format())

  throw new Error('Invalid environment variables')
}

export const env = _env.data

Como o process.env é retornado como um objeto, o que permite validar esse objeto em conformidade com o enviromnentSchema e, caso não cumpra o "contrato" será lançado um erro.

Passo 3: Utilização

A utilizaração é bem simples:

import express from 'express'
import { env } from '../config/env' // caminho para a pasta em seu projeto

const app = express()

app.listen(env.PORT, () => {
  console.log('server running')
})

Agora, vamos para o queridinho Nest.

Passo 1: Instalação dos pacotes - Nest

Nesse exemplo, criaremos um modulo, um serviço e um esquema para isso mas antes será necessário instalar o pacote abaixo:

npm i --save @nestjs/config
Passo 2: Criação do schema

Para o arquivo env, teremos algumas mudanças:

//env.ts
export const envSchema = z.object({
  DB_HOST: z.string(),
  DB_PORT: z.coerce.number(),
  DB_USERNAME: z.string(),
  DB_PASSWORD: z.string(),
  DB_NAME: z.string(),
  PORT: z.coerce.number().optional().default(3000)
});

export type Env = z.infer<typeof envSchema>;
Passo 3: Trabalhando com módulos

Dentro do Nest, trabalhar com modulos e services é essencial. O modulo de env precisa ser visivel por toda aplicação, e o serviço precisa ser injetado em qualquer modulo em que for chamado.

Como assim injetar? serviço? Se está meio confuso, recomendo consultar a documentação do Nest antes de prosseguir com o artigo.

// env.service
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Env } from './env';
 
@Injectable()
export class EnvService {
  constructor(private configService: ConfigService<Env, true>) {}
  
  get<T extends keyof Env>(key: T) {
    return this.configService.get(key, { infer: true });
  }
}

No exemplo acima, é um serviço no nest, fazendo o uso do padrão de decorators conseguimos tornar esse serviço acessível e utilizável em diferentes partes da aplicação.

// env.module.ts
import { Module } from '@nestjs/common';
import { EnvService } from './env.service';

@Module({
  providers: [EnvService],
  exports: [EnvService],
})
export class EnvModule {}

Conforme mencionado anteriormente, é crucial informar à nossa aplicação que esse módulo deve ser global. Para realizar essa configuração, vamos até o módulo principal da aplicação:

// app.module.ts
import { ConfigModule } from '@nestjs/config';
import { envSchema } from './infra/env';

@Module({
  imports: [
    ConfigModule.forRoot({
      validate: (env) => envSchema.parse(env),
      isGlobal: true,
    }),
  ],
})
export class AppModule {}
Passo 4: Utilizando o serviço de variáveis

Mas e o arquivo main.ts, como teria acesso as váriaveis? como consigo iniciar minha aplicação na porta que eu desejo? Ao utilizar o app, podemos configurar e inicializar a aplicação conforme suas necessidades incluindo a porta que desejamos utilizar:

//main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(EnvService); //Acessando o service
  const port = configService.get('PORT'); //Pegando a variável {PORT}

  await app.listen(port);
}
bootstrap();

Por fim, caso seja necessário acessar alguma variável, podemos realizar a injeção desse serviço no módulo em que é necessário o acesso. Assim conseguimos garantir o uso variáveis.

// database.module.ts
import { EnvModule, EnvService } from '../env';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [EnvModule],
      inject: [EnvService],
      useClass: TypeOrmConfigService,
    }),
  ],
})
export class DatabaseModule {}
//typeorm.service.ts
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private config: EnvService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'postgres',
      host: this.config.get('DB_HOST'),
      port: this.config.get('DB_PORT'),
      username: this.config.get('DB_USERNAME'),
      password: this.config.get('DB_PASSWORD'),
      database: this.config.get('DB_NAME'),
      entities,
      synchronize: true,
      dropSchema: true,
      logging: true,
    };
  }
}
Conclusão

Para concluir, o objetivo deste artigo é apresentar uma abordagem para lidar com variáveis de ambiente, tanto no Nest quanto no Node. Espero que alguma informaçõe fornecida seja útil e que agora você possa utilizar ou aprimorar esse modo em suas aplicações. Se houver dúvidas ou sugestões, sinta-se à vontade para discutir!