Além do básico: Uso de Variáveis de Ambiente em Aplicações Node e Nest
Tags: nestjs, env, node, javascript
Published: 2024-02-02 14:33:45

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](https://zod.dev/) e [dotenv](https://www.dotenv.org/):
```bash
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. 
```ts
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?

```ts
// 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: 
```ts
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:

```bash
npm i --save @nestjs/config
```

##### Passo 2: Criação do schema
Para o arquivo env, teremos algumas mudanças: 
```ts
//env.ts
import { z } from 'zod';

export const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().optional().default(3000),
});

export const validateEnv = (env: Record<string, any>) => {
  const _env = envSchema.safeParse(process.env); //

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

    throw new Error('Invalid environment variables');
  }
  return _env.data;
};

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](https://docs.nestjs.com/) antes de prosseguir com o artigo.

```ts
// 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.

```ts
// 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:

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

@Module({
  imports: [
    ConfigModule.forRoot({
      validate: (env) => validateEnv(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 variáveis? 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:

```ts
//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.

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

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [EnvModule],
      inject: [EnvService],
      useClass: TypeOrmConfigService,
    }),
  ],
})
export class DatabaseModule {}
```

```ts
//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!