diff --git a/drips/.env.example b/drips/.env.example index d768fe0..355bccd 100644 --- a/drips/.env.example +++ b/drips/.env.example @@ -7,4 +7,4 @@ SMTP_PASSWORD=your-app-password SMTP_FROM=skills # Application URL -APP_URL=http://localhost:3000 \ No newline at end of file +APP_URL=http://localhost:3000 diff --git a/drips/README.md b/drips/README.md index 8f0f65f..4aaf0b6 100644 --- a/drips/README.md +++ b/drips/README.md @@ -93,6 +93,40 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors - Website - [https://nestjs.com](https://nestjs.com/) - Twitter - [@nestframework](https://twitter.com/nestframework) +## Configuration + +The application uses a centralized configuration module with environment variable validation using Joi. + +### Environment Files +The application loads environment variables from `.env` files. You can use different files for different environments: +- `.env`: Default environment variables +- `.env.development`: Development-specific variables +- `.env.test`: Test-specific variables + +### Required Variables +See [.env.example](.env.example) for the list of required environment variables. Key variables include: +- `PORT`: Server port (default: 3000) +- `NODE_ENV`: Environment mode (`development`, `production`, `test`) +- `DATABASE_HOST`: Database host +- `DATABASE_PORT`: Database port +- `DATABASE_USER`: Database user +- `DATABASE_PASSWORD`: Database password +- `DATABASE_NAME`: Database name + +### Validation +Environment variables are validated at startup. If any required variable is missing or malformed, the application will fail to boot with a descriptive error message. + +### Usage +To access configuration in other modules, inject `AppConfigService`: + +```typescript +import { AppConfigService } from './config/config.service'; + +constructor(private configService: AppConfigService) { + const port = this.configService.port; +} +``` + ## License Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/drips/package-lock.json b/drips/package-lock.json index f4f42b0..e4253eb 100644 --- a/drips/package-lock.json +++ b/drips/package-lock.json @@ -14,8 +14,10 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "@types/joi": "^17.2.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", + "joi": "^18.0.2", "pg": "^8.17.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -943,6 +945,54 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2470,6 +2520,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -2704,6 +2760,15 @@ "pretty-format": "^30.0.0" } }, + "node_modules/@types/joi": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", + "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", + "license": "MIT", + "dependencies": { + "joi": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -7053,6 +7118,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/drips/package.json b/drips/package.json index 8c2c6d5..c2ac4a4 100644 --- a/drips/package.json +++ b/drips/package.json @@ -25,8 +25,10 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "@nestjs/typeorm": "^11.0.0", + "@types/joi": "^17.2.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", + "joi": "^18.0.2", "pg": "^8.17.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/drips/src/app.module.ts b/drips/src/app.module.ts index b9c1d92..bfbebb6 100644 --- a/drips/src/app.module.ts +++ b/drips/src/app.module.ts @@ -1,28 +1,26 @@ import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AppConfigModule } from './config/config.module'; +import { AppConfigService } from './config/config.service'; @Module({ imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.env', - }), + AppConfigModule, TypeOrmModule.forRootAsync({ - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ + inject: [AppConfigService], + useFactory: (configService: AppConfigService) => ({ type: 'postgres', - host: configService.get('DATABASE_HOST', 'localhost'), - port: configService.get('DATABASE_PORT', 5432), - username: configService.get('DATABASE_USER', 'postgres'), - password: configService.get('DATABASE_PASSWORD', 'postgres'), - database: configService.get('DATABASE_NAME', 'drips_db'), + host: configService.databaseHost, + port: configService.databasePort, + username: configService.databaseUser, + password: configService.databasePassword, + database: configService.databaseName, entities: ['dist/**/*.entity{.ts,.js}'], migrations: ['dist/migrations/*{.ts,.js}'], - synchronize: configService.get('NODE_ENV') === 'development', - logging: configService.get('NODE_ENV') === 'development', + synchronize: configService.nodeEnv === 'development', + logging: configService.nodeEnv === 'development', // namingStrategy: new (require('typeorm').SnakeNamingStrategy)(), }), }), diff --git a/drips/src/config/config.module.ts b/drips/src/config/config.module.ts new file mode 100644 index 0000000..c66da7e --- /dev/null +++ b/drips/src/config/config.module.ts @@ -0,0 +1,21 @@ +import { Module, Global } from '@nestjs/common'; +import { ConfigModule as NestConfigModule } from '@nestjs/config'; +import { AppConfigService } from './config.service'; +import { envValidationSchema } from './env.validation'; + +@Global() +@Module({ + imports: [ + NestConfigModule.forRoot({ + isGlobal: true, + validationSchema: envValidationSchema, + validationOptions: { + allowUnknown: true, + abortEarly: true, + }, + }), + ], + providers: [AppConfigService], + exports: [AppConfigService], +}) +export class AppConfigModule {} diff --git a/drips/src/config/config.service.ts b/drips/src/config/config.service.ts new file mode 100644 index 0000000..d1f3191 --- /dev/null +++ b/drips/src/config/config.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService as NestConfigService } from '@nestjs/config'; + +@Injectable() +export class AppConfigService { + constructor(private configService: NestConfigService) {} + + get nodeEnv(): string { + return this.configService.get('NODE_ENV')!; + } + + get port(): number { + return this.configService.get('PORT')!; + } + + get databaseHost(): string { + return this.configService.get('DATABASE_HOST')!; + } + + get databasePort(): number { + return this.configService.get('DATABASE_PORT')!; + } + + get databaseUser(): string { + return this.configService.get('DATABASE_USER')!; + } + + get databasePassword(): string { + return this.configService.get('DATABASE_PASSWORD')!; + } + + get databaseName(): string { + return this.configService.get('DATABASE_NAME')!; + } + + get redisHost(): string { + return this.configService.get('REDIS_HOST')!; + } + + get redisPort(): number { + return this.configService.get('REDIS_PORT')!; + } + + get redisPassword(): string | undefined { + return this.configService.get('REDIS_PASSWORD'); + } + + get redisDb(): number { + return this.configService.get('REDIS_DB')!; + } + + get(key: string): any { + return this.configService.get(key); + } +} diff --git a/drips/src/config/env.validation.ts b/drips/src/config/env.validation.ts new file mode 100644 index 0000000..43fd24d --- /dev/null +++ b/drips/src/config/env.validation.ts @@ -0,0 +1,17 @@ +import * as Joi from 'joi'; + +export const envValidationSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'production', 'test', 'provision') + .default('development'), + PORT: Joi.number().default(3000), + DATABASE_HOST: Joi.string().required(), + DATABASE_PORT: Joi.number().default(5432), + DATABASE_USER: Joi.string().required(), + DATABASE_PASSWORD: Joi.string().required(), + DATABASE_NAME: Joi.string().required(), + REDIS_HOST: Joi.string().default('localhost'), + REDIS_PORT: Joi.number().default(6379), + REDIS_PASSWORD: Joi.string().allow('').optional(), + REDIS_DB: Joi.number().default(0), +}); diff --git a/drips/src/main.ts b/drips/src/main.ts index 90a498a..1b1f678 100644 --- a/drips/src/main.ts +++ b/drips/src/main.ts @@ -1,6 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { AppModule } from './app.module'; +import { AppConfigService } from './config/config.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -14,8 +15,11 @@ async function bootstrap() { }), ); - await app.listen(process.env.PORT ?? 3000, () => { - console.log(`Server running on http://localhost:${process.env.PORT ?? 3000}`); + const configService = app.get(AppConfigService); + const port = configService.port; + + await app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); }); } bootstrap();