From 217c48663b194b941c02ae420e593080ab3a1715 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 19 Mar 2026 12:38:28 +0100 Subject: [PATCH] feat(swagger): add OpenAPI documentation to calendar, contacts, and todo backends - Extend shared-nestjs-setup bootstrapApp with optional swagger config - Auto-setup Swagger/OpenAPI when swagger: true is passed - Add @nestjs/swagger as optional peer dependency - Enable swagger in calendar (:3014/api/docs), contacts (:3015/api/docs), todo (:3018/api/docs) - Migrate todo main.ts from custom bootstrap to shared bootstrapApp - JWT Bearer auth configured in Swagger UI Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/calendar/apps/backend/package.json | 1 + apps/calendar/apps/backend/src/main.ts | 1 + apps/contacts/apps/backend/package.json | 1 + apps/contacts/apps/backend/src/main.ts | 1 + apps/todo/apps/backend/package.json | 1 + apps/todo/apps/backend/src/main.ts | 72 +++-------------------- packages/shared-nestjs-setup/package.json | 8 ++- packages/shared-nestjs-setup/src/index.ts | 60 ++++++++++++++++++- pnpm-lock.yaml | 18 ++++++ 9 files changed, 94 insertions(+), 69 deletions(-) diff --git a/apps/calendar/apps/backend/package.json b/apps/calendar/apps/backend/package.json index 93a89dfd9..1736ffa7f 100644 --- a/apps/calendar/apps/backend/package.json +++ b/apps/calendar/apps/backend/package.json @@ -33,6 +33,7 @@ "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", "@nestjs/schedule": "^4.1.2", + "@nestjs/swagger": "^11.2.6", "@nestjs/throttler": "^6.2.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", diff --git a/apps/calendar/apps/backend/src/main.ts b/apps/calendar/apps/backend/src/main.ts index b695fe495..fa0bfdc02 100644 --- a/apps/calendar/apps/backend/src/main.ts +++ b/apps/calendar/apps/backend/src/main.ts @@ -5,4 +5,5 @@ bootstrapApp(AppModule, { defaultPort: 3014, serviceName: 'Calendar', additionalCorsOrigins: ['http://localhost:5179'], + swagger: true, }); diff --git a/apps/contacts/apps/backend/package.json b/apps/contacts/apps/backend/package.json index 7cef259df..7ade78b57 100644 --- a/apps/contacts/apps/backend/package.json +++ b/apps/contacts/apps/backend/package.json @@ -30,6 +30,7 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", + "@nestjs/swagger": "^11.2.6", "@nestjs/throttler": "^6.2.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", diff --git a/apps/contacts/apps/backend/src/main.ts b/apps/contacts/apps/backend/src/main.ts index 4d33d03f8..03a02b127 100644 --- a/apps/contacts/apps/backend/src/main.ts +++ b/apps/contacts/apps/backend/src/main.ts @@ -5,4 +5,5 @@ bootstrapApp(AppModule, { defaultPort: 3015, serviceName: 'Contacts', additionalCorsOrigins: ['http://localhost:5184'], + swagger: true, }); diff --git a/apps/todo/apps/backend/package.json b/apps/todo/apps/backend/package.json index 02a8540f4..c94651dc5 100644 --- a/apps/todo/apps/backend/package.json +++ b/apps/todo/apps/backend/package.json @@ -28,6 +28,7 @@ "@nestjs/core": "^10.4.9", "@nestjs/platform-express": "^10.4.9", "@nestjs/schedule": "^4.1.2", + "@nestjs/swagger": "^11.2.6", "@nestjs/throttler": "^6.2.1", "@todo/shared": "workspace:*", "class-transformer": "^0.5.1", diff --git a/apps/todo/apps/backend/src/main.ts b/apps/todo/apps/backend/src/main.ts index 75c1b53fc..5ae8773d9 100644 --- a/apps/todo/apps/backend/src/main.ts +++ b/apps/todo/apps/backend/src/main.ts @@ -1,67 +1,9 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe, Logger } from '@nestjs/common'; +import { bootstrapApp } from '@manacore/shared-nestjs-setup'; import { AppModule } from './app.module'; -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - // Enable CORS for all platforms - app.enableCors({ - origin: (origin, callback) => { - // Allow requests with no origin (mobile apps, curl, etc.) - if (!origin) { - callback(null, true); - return; - } - - const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [ - 'http://localhost:5173', - 'http://localhost:5186', - 'http://localhost:8081', - 'http://localhost:19006', - ]; - - // Allow all localhost ports in development - if (process.env.NODE_ENV === 'development' && origin.includes('localhost')) { - callback(null, true); - return; - } - - if (allowedOrigins.includes(origin)) { - callback(null, true); - } else { - logger.warn(`Blocked request from origin: ${origin}`); - callback(new Error('Not allowed by CORS'), false); - } - }, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - }); - - // Global validation pipe - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - transformOptions: { - enableImplicitConversion: true, - }, - }) - ); - - // API prefix (exclude metrics endpoint for Prometheus scraping) - app.setGlobalPrefix('api/v1', { - exclude: ['metrics', 'health'], - }); - - const port = process.env.PORT || 3017; - await app.listen(port); - - logger.log(`Todo API is running on: http://localhost:${port}`); - logger.log(`Health check: http://localhost:${port}/health`); -} - -bootstrap(); +bootstrapApp(AppModule, { + defaultPort: 3018, + serviceName: 'Todo', + additionalCorsOrigins: ['http://localhost:5186', 'http://localhost:5188'], + swagger: true, +}); diff --git a/packages/shared-nestjs-setup/package.json b/packages/shared-nestjs-setup/package.json index 170b324a6..5bbb6abab 100644 --- a/packages/shared-nestjs-setup/package.json +++ b/packages/shared-nestjs-setup/package.json @@ -30,6 +30,12 @@ }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", - "@nestjs/core": "^10.0.0 || ^11.0.0" + "@nestjs/core": "^10.0.0 || ^11.0.0", + "@nestjs/swagger": "^8.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/swagger": { + "optional": true + } } } diff --git a/packages/shared-nestjs-setup/src/index.ts b/packages/shared-nestjs-setup/src/index.ts index b346181fb..a6dae04e8 100644 --- a/packages/shared-nestjs-setup/src/index.ts +++ b/packages/shared-nestjs-setup/src/index.ts @@ -1,8 +1,8 @@ /** * Shared NestJS Bootstrap Utilities for ManaCore Backends * - * Provides a consistent setup for CORS, validation, and global prefix - * across all backend applications. + * Provides a consistent setup for CORS, validation, global prefix, + * and optional Swagger/OpenAPI documentation across all backend applications. */ import { type INestApplication, ValidationPipe, type Type } from '@nestjs/common'; @@ -21,6 +21,20 @@ export const DEFAULT_CORS_ORIGINS = [ 'exp://localhost:8081', // Expo native ]; +/** + * Swagger/OpenAPI configuration + */ +export interface SwaggerOptions { + /** API title (default: serviceName + ' API') */ + title?: string; + /** API description */ + description?: string; + /** API version (default: '1.0') */ + version?: string; + /** Path to serve docs at (default: 'api/docs') */ + path?: string; +} + /** * Configuration options for the bootstrap utility */ @@ -39,6 +53,8 @@ export interface BootstrapOptions { corsMethods?: string[]; /** Body size limit for JSON/urlencoded payloads (default: '100kb'). Use '50mb' for image uploads. */ bodyLimit?: string; + /** Enable Swagger/OpenAPI docs. Pass true for defaults or SwaggerOptions for custom config. */ + swagger?: boolean | SwaggerOptions; } /** @@ -93,6 +109,38 @@ export function configurePrefix( app.setGlobalPrefix(prefix, { exclude }); } +/** + * Setup Swagger/OpenAPI documentation if enabled and @nestjs/swagger is installed + */ +async function setupSwagger( + app: INestApplication, + serviceName: string, + port: string | number, + swaggerConfig: boolean | SwaggerOptions +): Promise { + try { + const { DocumentBuilder, SwaggerModule } = await import('@nestjs/swagger'); + const opts: SwaggerOptions = typeof swaggerConfig === 'object' ? swaggerConfig : {}; + + const config = new DocumentBuilder() + .setTitle(opts.title || `${serviceName} API`) + .setDescription(opts.description || `API documentation for ${serviceName}`) + .setVersion(opts.version || '1.0') + .addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'JWT-auth') + .build(); + + const document = SwaggerModule.createDocument(app, config); + const docsPath = opts.path || 'api/docs'; + SwaggerModule.setup(docsPath, app, document, { + swaggerOptions: { persistAuthorization: true }, + }); + + console.log(`${serviceName} API docs at http://localhost:${port}/${docsPath}`); + } catch { + // @nestjs/swagger not installed - skip silently + } +} + /** * Bootstrap a NestJS application with standard configuration * @@ -105,6 +153,7 @@ export function configurePrefix( * defaultPort: 3002, * serviceName: 'Chat', * additionalCorsOrigins: ['http://localhost:5178'], + * swagger: true, * }); * ``` */ @@ -135,8 +184,13 @@ export async function bootstrapApp( options.excludeFromPrefix ?? ['metrics', 'health'] ); - // Start listening + // Setup Swagger/OpenAPI docs if enabled const port = process.env.PORT || options.defaultPort; + if (options.swagger) { + await setupSwagger(app, options.serviceName, port, options.swagger); + } + + // Start listening await app.listen(port); console.log(`${options.serviceName} backend running on http://localhost:${port}`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c6630ec2..0d49c6ff8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,6 +467,9 @@ importers: '@nestjs/platform-express': specifier: ^10.4.15 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/throttler': + specifier: ^6.2.1 + version: 6.4.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -1300,6 +1303,9 @@ importers: '@manacore/shared-vite-config': specifier: workspace:* version: link:../../../../packages/shared-vite-config + '@playwright/test': + specifier: ^1.52.0 + version: 1.57.0 '@sveltejs/adapter-node': specifier: ^5.0.0 version: 5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))) @@ -2726,6 +2732,9 @@ importers: '@nestjs/platform-express': specifier: ^10.4.15 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/throttler': + specifier: ^6.2.1 + version: 6.4.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -3487,6 +3496,9 @@ importers: '@nestjs/platform-express': specifier: ^10.4.15 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/throttler': + specifier: ^6.2.1 + version: 6.4.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -5324,6 +5336,9 @@ importers: '@manacore/shared-vite-config': specifier: workspace:* version: link:../../../../packages/shared-vite-config + '@playwright/test': + specifier: ^1.52.0 + version: 1.57.0 '@sveltejs/adapter-node': specifier: ^5.0.0 version: 5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))) @@ -6423,6 +6438,9 @@ importers: '@nestjs/core': specifier: ^10.0.0 || ^11.0.0 version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/swagger': + specifier: ^8.0.0 || ^7.0.0 + version: 8.1.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) express: specifier: ^4.21.0 version: 4.21.2