From d5b2db1aad618239e7ee9aa8f654f82dbae38b09 Mon Sep 17 00:00:00 2001 From: Mohammed Gomaa Date: Fri, 18 Oct 2024 16:02:15 +0300 Subject: [PATCH] refactor(env-vars): add validation (#56) --- .github/workflows/ci.yml | 2 +- apps/api/.env.sample | 12 ++++++ apps/api/src/app.module.ts | 4 +- apps/api/src/main.ts | 14 ++++--- apps/api/src/utils/env.validate.ts | 61 ++++++++++++++++++++++++++++++ docker-compose.dev.yml | 16 ++++++++ package.json | 2 +- 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/utils/env.validate.ts create mode 100644 docker-compose.dev.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e6689a..eb38240 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: run: pnpm run compose:up - name: Running API Tests working-directory: ./apps/api - run: pnpm run test + run: pnpm run test && pnpm run test:e2e - name: Upload results to Codecov uses: codecov/codecov-action@v4 with: diff --git a/apps/api/.env.sample b/apps/api/.env.sample index f3e8f8b..1276ab9 100644 --- a/apps/api/.env.sample +++ b/apps/api/.env.sample @@ -1,4 +1,16 @@ +# Server +PORT=3000 +NODE_ENV=development + +# Database DATABASE_URL=postgres://postgres:pass1234@localhost:5432/disworse POSTGRES_DB=disworse POSTGRES_USER=postgres POSTGRES_PASSWORD=pass1234 + +# Redis +REDIS_URL=redis://localhost:6379 + +# Auth +SESSION_SECRET=secret +COOKIE_MAX_AGE=604800000 diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index aa2e49f..2eda15c 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -10,12 +10,14 @@ import { AppService } from "./app.service"; import { AuthenticatedGuard } from "./common/guards/auth.guard"; import { DrizzleModule } from "./drizzle/drizzle.module"; import { AuthModule } from "./modules/auth/auth.module"; +import { validate } from "./utils/env.validate"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, - envFilePath: "../../../.env.backend", + envFilePath: ".env", + validate, }), GraphQLModule.forRoot({ driver: ApolloDriver, diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 38671fc..8497ba7 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,4 +1,5 @@ import { ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; import { NestFactory } from "@nestjs/core"; import RedisStore from "connect-redis"; import * as session from "express-session"; @@ -8,7 +9,8 @@ import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.setGlobalPrefix("api"); + const configService = app.get(ConfigService); + app.useGlobalPipes( new ValidationPipe({ transform: true, @@ -17,16 +19,16 @@ async function bootstrap() { ); const redisClient = await createClient({ - url: String(process.env.REDIS_URL), + url: String(configService.getOrThrow("REDIS_URL")), }).connect(); app.use( session({ - secret: String(process.env.SESSION_SECRET), + secret: configService.getOrThrow("SESSION_SECRET"), resave: false, saveUninitialized: false, cookie: { - maxAge: Number(process.env.COOKIE_MAX_AGE), + maxAge: configService.getOrThrow("COOKIE_MAX_AGE"), httpOnly: true, }, store: new RedisStore({ @@ -37,8 +39,8 @@ async function bootstrap() { app.use(passport.initialize()); app.use(passport.session()); - app.enableCors(); - await app.listen(process.env.PORT || 3333); + + await app.listen(configService.get("PORT") || 3333); } bootstrap(); diff --git a/apps/api/src/utils/env.validate.ts b/apps/api/src/utils/env.validate.ts new file mode 100644 index 0000000..92c7b6e --- /dev/null +++ b/apps/api/src/utils/env.validate.ts @@ -0,0 +1,61 @@ +import { plainToInstance } from "class-transformer"; +import { + IsBoolean, + IsEnum, + IsNumber, + IsString, + Max, + Min, + validateSync, +} from "class-validator"; + +enum Environment { + Development = "development", + Production = "production", + Test = "test", +} + +export class EnvironmentVariables { + @IsEnum(Environment) + NODE_ENV: Environment; + + @IsNumber() + @Min(0) + @Max(65535) + PORT: number; + + @IsString() + DATABASE_URL: string; + + @IsString() + POSTGRES_DB: string; + + @IsString() + POSTGRES_USER: string; + + @IsString() + POSTGRES_PASSWORD: string; + + @IsString() + REDIS_URL: string; + + @IsString() + SESSION_SECRET: string; + + @IsNumber() + COOKIE_MAX_AGE: number; +} + +export function validate(config: Record) { + const validatedConfig = plainToInstance(EnvironmentVariables, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new Error(errors.toString()); + } + return validatedConfig; +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..feb801d --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,16 @@ +services: + pg: + image: postgres:15-alpine + container_name: postgres-dev + restart: always + ports: + - 5432:5432 + env_file: + - ./apps/api/.env + + cache: + image: redis:7.2.4-alpine + container_name: cache-dev + restart: always + ports: + - 6379:6379 diff --git a/package.json b/package.json index a28407b..f3e6fa5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint:fix:unsafe": "biome check . --write --unsafe", "prepare": "husky", "commitlint": "commitlint --edit", - "compose:up": "docker compose up -d --build", + "compose:up": "docker compose -f docker-compose.dev.yml up -d --build", "compose:down": "docker compose down -v" }, "lint-staged": {