From 58bf4c284c5635e4cdc87d325af6ed26a67ac34f Mon Sep 17 00:00:00 2001 From: JerryIdoko Date: Thu, 26 Mar 2026 10:58:19 +0100 Subject: [PATCH] docs: implement Swagger UI, API versioning, and usage examples (#119) --- .../middleware/api-version.middleware.ts | 6 +-- src/main.ts | 24 ++++++++-- src/transactions/transactions.controller.ts | 29 +++++++++++- src/valuation/valuation.controller.ts | 47 +++++++------------ 4 files changed, 68 insertions(+), 38 deletions(-) diff --git a/src/common/api-version/middleware/api-version.middleware.ts b/src/common/api-version/middleware/api-version.middleware.ts index 1b21190d..c5c67342 100644 --- a/src/common/api-version/middleware/api-version.middleware.ts +++ b/src/common/api-version/middleware/api-version.middleware.ts @@ -28,9 +28,9 @@ export const VERSION_HEADER = 'Accept-Version'; export const VERSION_QUERY_PARAM = 'version'; /** - * Path pattern to extract version from URL + * Path pattern to extract version from URL (matches /v1.0/ or /v1.0 at start or after /) */ -const VERSION_PATH_PATTERN = /^\/v(\d+\.\d+)/; +const VERSION_PATH_PATTERN = /\/v(\d+\.\d+)(?:\/|$)/; @Injectable() export class ApiVersionMiddleware implements NestMiddleware { @@ -92,7 +92,7 @@ export class ApiVersionMiddleware implements NestMiddleware { */ private extractFromPath(path: string): string | null { const match = path.match(VERSION_PATH_PATTERN); - return match ? `1.${match[1]}` : null; + return match ? match[1] : null; } /** diff --git a/src/main.ts b/src/main.ts index bf2b12d7..a666e370 100644 --- a/src/main.ts +++ b/src/main.ts @@ -126,10 +126,20 @@ async function bootstrap() { .setTitle('PropChain API') .setDescription('Decentralized Real Estate Infrastructure - Backend API') .setVersion(DEFAULT_API_VERSION) - .addTag('properties') - .addTag('transactions') - .addTag('users') - .addTag('blockchain') + .addTag('auth', 'Authentication and Authorization') + .addTag('users', 'User management and profiles') + .addTag('properties', 'Property listings and management') + .addTag('transactions', 'Escrowed real estate transactions') + .addTag('valuation', 'Automated property valuation and market trends') + .addTag('documents', 'Secure document storage and versioning') + .addTag('blockchain', 'Web3 and smart contract interactions') + .addTag('Audit & Compliance', 'Regulatory auditing and compliance reporting') + .addTag('search', 'Advanced search across properties and users') + .addTag('security', 'Security headers and system safety') + .addTag('health', 'System health and status monitoring') + .addTag('backup-recovery', 'Disaster recovery and backup management') + .addTag('API Keys', 'Key management for external integrations') + .addTag('feature-flags', 'Dynamic feature toggles') .addBearerAuth() .addApiKey({ type: 'apiKey', name: 'X-API-KEY', in: 'header' }, 'apiKey') .addApiKey({ type: 'apiKey', name: 'Accept-Version', in: 'header' }, 'version') @@ -142,6 +152,12 @@ async function bootstrap() { customSiteTitle: 'PropChain API Documentation', customCss: '.swagger-ui .topbar { display: none }', customfavIcon: '/favicon.ico', + swaggerOptions: { + persistAuthorization: true, + docExpansion: 'none', + filter: true, + showRequestDuration: true, + }, }); logger.log(`Swagger documentation available at /${apiPrefix}/docs`); diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts index ff9b9a75..0f3a1613 100644 --- a/src/transactions/transactions.controller.ts +++ b/src/transactions/transactions.controller.ts @@ -1,33 +1,60 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBearerAuth } from '@nestjs/swagger'; import { TransactionsService } from './transactions.service'; import { CreateTransactionDto, DisputeDto } from './dto/create-transaction.dto'; import { TransactionQueryDto } from './dto/transaction-query.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +@ApiTags('transactions') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) @Controller('transactions') export class TransactionsController { constructor(private readonly service: TransactionsService) {} @Post() + @ApiOperation({ summary: 'Create a new transaction' }) + @ApiResponse({ status: 201, description: 'Transaction created successfully.', type: CreateTransactionDto }) + @ApiResponse({ status: 400, description: 'Invalid transaction data.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) create(@Body() dto: CreateTransactionDto) { return this.service.createTransaction(dto); } @Post(':id/escrow') + @ApiOperation({ summary: 'Fund escrow for a transaction' }) + @ApiParam({ name: 'id', description: 'Transaction ID' }) + @ApiResponse({ status: 200, description: 'Escrow funded successfully.' }) + @ApiResponse({ status: 404, description: 'Transaction not found.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) fundEscrow(@Param('id') id: string) { return this.service.fundEscrow(id); } @Get(':id') + @ApiOperation({ summary: 'Get transaction by ID' }) + @ApiParam({ name: 'id', description: 'Transaction ID' }) + @ApiResponse({ status: 200, description: 'Transaction found.' }) + @ApiResponse({ status: 404, description: 'Transaction not found.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) findOne(@Param('id') id: string) { return this.service.getTransaction(id); } @Get() + @ApiOperation({ summary: 'List transactions with filters' }) + @ApiResponse({ status: 200, description: 'List of transactions.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) findAll(@Query() query: TransactionQueryDto) { return this.service.findAll(query); } @Post(':id/dispute') + @ApiOperation({ summary: 'Raise a dispute for a transaction' }) + @ApiParam({ name: 'id', description: 'Transaction ID' }) + @ApiResponse({ status: 200, description: 'Dispute raised successfully.', type: DisputeDto }) + @ApiResponse({ status: 404, description: 'Transaction not found.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) dispute(@Param('id') id: string, @Body() dto: DisputeDto) { return this.service.raiseDispute(id, dto); } diff --git a/src/valuation/valuation.controller.ts b/src/valuation/valuation.controller.ts index 2b26f14b..e18e7834 100644 --- a/src/valuation/valuation.controller.ts +++ b/src/valuation/valuation.controller.ts @@ -1,11 +1,15 @@ -import { Controller, Get, Post, Param, Body, ValidationPipe, HttpCode, HttpStatus, Logger } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; +import { Controller, Get, Post, Param, Body, ValidationPipe, HttpCode, HttpStatus, Logger, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiBearerAuth } from '@nestjs/swagger'; import { ValuationService } from './valuation.service'; import { ValuationResult } from './valuation.types'; import { PropertyFeaturesDto } from './dto/property-features.dto'; import { BatchValuationRequestDto } from './dto/batch-valuation-request.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { ApiStandardErrorResponse } from '../common/errors/api-standard-error-response.decorator'; @ApiTags('valuation') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) @Controller('valuation') export class ValuationController { private readonly logger = new Logger(ValuationController.name); @@ -15,10 +19,9 @@ export class ValuationController { @Post(':propertyId') @ApiOperation({ summary: 'Get property valuation' }) @ApiParam({ name: 'propertyId', description: 'ID of the property to value' }) - @ApiBody({ description: 'Property features for valuation', required: false }) + @ApiBody({ type: PropertyFeaturesDto, description: 'Property features for valuation', required: false }) @ApiResponse({ status: 200, description: 'Property valuation successful' }) - @ApiResponse({ status: 404, description: 'Property not found' }) - @ApiResponse({ status: 422, description: 'Invalid property features' }) + @ApiStandardErrorResponse([400, 401, 404, 422]) @HttpCode(HttpStatus.OK) async getValuation( @Param('propertyId') propertyId: string, @@ -32,7 +35,7 @@ export class ValuationController { @ApiOperation({ summary: 'Get property valuation history' }) @ApiParam({ name: 'propertyId', description: 'ID of the property' }) @ApiResponse({ status: 200, description: 'Property valuation history retrieved' }) - @ApiResponse({ status: 404, description: 'Property not found' }) + @ApiStandardErrorResponse([401, 404]) async getPropertyHistory(@Param('propertyId') propertyId: string): Promise { this.logger.log(`Requesting valuation history for property ${propertyId}`); return this.valuationService.getPropertyHistory(propertyId); @@ -40,19 +43,19 @@ export class ValuationController { @Get('trends/:location') @ApiOperation({ summary: 'Get market trend analysis for a location' }) - @ApiParam({ name: 'location', description: 'Location to analyze market trends' }) + @ApiParam({ name: 'location', description: 'Location (address or ZIP) to analyze market trends' }) @ApiResponse({ status: 200, description: 'Market trend analysis retrieved' }) - @ApiResponse({ status: 404, description: 'Location not found in records' }) + @ApiStandardErrorResponse([400, 401, 404]) async getMarketTrendAnalysis(@Param('location') location: string) { this.logger.log(`Requesting market trend analysis for location ${location}`); return this.valuationService.getMarketTrendAnalysis(location); } @Get(':propertyId/latest') - @ApiOperation({ summary: 'Get latest valuation for a property' }) + @ApiOperation({ summary: 'Get latest valuation for a property', description: 'Retrieves the most recent valuation from history.' }) @ApiParam({ name: 'propertyId', description: 'ID of the property' }) @ApiResponse({ status: 200, description: 'Latest valuation retrieved' }) - @ApiResponse({ status: 404, description: 'Property not found' }) + @ApiStandardErrorResponse([401, 404]) async getLatestValuation(@Param('propertyId') propertyId: string): Promise { const history = await this.valuationService.getPropertyHistory(propertyId); if (history.length === 0) { @@ -62,26 +65,10 @@ export class ValuationController { } @Post('batch') - @ApiOperation({ summary: 'Get valuations for multiple properties' }) - @ApiBody({ - description: 'Array of property IDs and features', - schema: { - type: 'object', - properties: { - properties: { - type: 'array', - items: { - type: 'object', - properties: { - propertyId: { type: 'string' }, - features: { $ref: '#/components/schemas/PropertyFeatures' }, - }, - }, - }, - }, - }, - }) - @ApiResponse({ status: 200, description: 'Batch valuations retrieved' }) + @ApiOperation({ summary: 'Get valuations for multiple properties', description: 'Processes a batch of property IDs and optional features for valuation.' }) + @ApiBody({ type: BatchValuationRequestDto }) + @ApiResponse({ status: 200, description: 'Batch valuations processed.' }) + @ApiStandardErrorResponse([400, 401]) @HttpCode(HttpStatus.OK) async getBatchValuations( @Body() requestBody: BatchValuationRequestDto,