chore: env validation, shutdown hooks, logger noise reduction

- add Zod-based env validation schema; fail fast on missing/invalid vars
- suppress NestJS framework boot logs (InstanceLoader, RouterExplorer, RoutesResolver)
- enable graceful shutdown hooks
This commit is contained in:
neon
2026-02-28 19:51:06 -06:00
parent 51dd3d30f3
commit 9c1b57c72b
4 changed files with 51 additions and 0 deletions
+2
View File
@@ -5,6 +5,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import { LoggerModule } from 'nestjs-pino';
import { appConfig, authConfig, dbConfig, externalApiConfig, mailerConfig, storageConfig } from './config/config';
import { validateEnv } from './config/env.validation';
import { loggerConfig } from './common/logger.config';
import { CommonModule } from './common/common.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
@@ -44,6 +45,7 @@ import { UserModule } from './modules/user/user.module';
LoggerModule.forRoot(loggerConfig),
ConfigModule.forRoot({
isGlobal: true,
validate: validateEnv,
load: [appConfig, dbConfig, authConfig, storageConfig, mailerConfig, externalApiConfig],
}),
ScheduleModule.forRoot(),
+12
View File
@@ -3,9 +3,21 @@ import type { IncomingMessage, ServerResponse } from 'http';
const isDev = process.env.NODE_ENV !== 'production';
const FRAMEWORK_CONTEXTS = new Set(['InstanceLoader', 'RouterExplorer', 'RoutesResolver']);
export const loggerConfig: Params = {
pinoHttp: {
level: isDev ? 'debug' : 'info',
hooks: {
logMethod(inputArgs, method) {
const first = inputArgs[0];
if (first !== null && typeof first === 'object' && 'context' in first && FRAMEWORK_CONTEXTS.has((first as Record<string, unknown>).context as string)) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return method.apply(this, inputArgs);
},
},
customProps: () => ({ context: 'HTTP' }),
customSuccessMessage: (req: IncomingMessage, res: ServerResponse, responseTime: number) => {
return `${req.method} ${req.url} ${res.statusCode} +${Math.round(responseTime)}ms`;
+36
View File
@@ -0,0 +1,36 @@
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
JWT_SECRET: z
.string()
.min(16, 'JWT_SECRET must be at least 16 characters')
.refine(
(val) => process.env.NODE_ENV !== 'production' || val !== 'change-me-in-production',
'JWT_SECRET must be changed from the default value in production',
),
JWT_EXPIRES_IN: z.string().default('15m'),
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
BOOKS_PATH: z.string().default('/data/books'),
STAGING_PATH: z.string().optional(),
CLIENT_URL: z.string().url().optional(),
GOOGLE_BOOKS_API_KEY: z.string().default(''),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().positive().default(587),
SMTP_SECURE: z.string().optional(),
SMTP_USER: z.string().optional(),
SMTP_PASS: z.string().optional(),
SMTP_FROM: z.string().default('noreply@projectx.local'),
APP_URL: z.string().url().default('http://localhost:5173'),
});
export function validateEnv(config: Record<string, unknown>) {
const result = envSchema.safeParse(config);
if (!result.success) {
const errors = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
throw new Error(`Environment validation failed:\n${errors}`);
}
return result.data;
}
+1
View File
@@ -119,6 +119,7 @@ async function bootstrap() {
});
}
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}