diff --git a/src/main.ts b/src/main.ts index bc391b0..2588aad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,24 @@ import { createWriteStream } from "fs"; import { get } from "http"; import { ValidationPipe } from "@nestjs/common"; -import { NestFactory } from "@nestjs/core"; +import { HttpAdapterHost, NestFactory } from "@nestjs/core"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "./app.module"; +import { PrismaClientExceptionFilter } from "./prisma-client-exception/prisma-client-exception.filter"; const DEFAULT_PORT = 3000; async function bootstrap(): Promise { const app = await NestFactory.create(AppModule); + // Validation Pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + // Exception Filter + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter)); + + // Swagger const config = new DocumentBuilder() .setTitle("Node + Nest + TypeScript starter project API") .setDescription("Basic User API generated with @nestjs/swagger") diff --git a/src/prisma-client-exception/prisma-client-exception.filter.spec.ts b/src/prisma-client-exception/prisma-client-exception.filter.spec.ts new file mode 100644 index 0000000..cbe53e6 --- /dev/null +++ b/src/prisma-client-exception/prisma-client-exception.filter.spec.ts @@ -0,0 +1,7 @@ +import { PrismaClientExceptionFilter } from "./prisma-client-exception.filter"; + +describe("PrismaClientExceptionFilter", () => { + it("should be defined", () => { + expect(new PrismaClientExceptionFilter()).toBeDefined(); + }); +}); diff --git a/src/prisma-client-exception/prisma-client-exception.filter.ts b/src/prisma-client-exception/prisma-client-exception.filter.ts new file mode 100644 index 0000000..f1e3198 --- /dev/null +++ b/src/prisma-client-exception/prisma-client-exception.filter.ts @@ -0,0 +1,36 @@ +import { + ArgumentsHost, + Catch, + HttpException, + HttpStatus, +} from "@nestjs/common"; +import { BaseExceptionFilter } from "@nestjs/core"; +import { Prisma } from "@prisma/client"; + +@Catch(Prisma.PrismaClientKnownRequestError) +export class PrismaClientExceptionFilter extends BaseExceptionFilter { + catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost): void { + const { code, message } = exception; + + // Log full error message on the server + console.error(message); + + // Trim error message for the client + const trimmedMessage = message.substring(message.lastIndexOf("\n") + 1); + + // Map Prisma Client exception codes to their corresponding HTTP status code + switch (code) { + case "P2000": + super.catch(new HttpException(trimmedMessage, HttpStatus.BAD_REQUEST), host); + break; + case "P2002": + super.catch(new HttpException(trimmedMessage, HttpStatus.CONFLICT), host); + break; + case "P2025": + super.catch(new HttpException(trimmedMessage, HttpStatus.NOT_FOUND), host); + break; + default: + super.catch(exception, host); + } + } +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index bf06707..e06b102 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -9,7 +9,10 @@ import { ParseIntPipe, } from "@nestjs/common"; import { + ApiBadRequestResponse, + ApiConflictResponse, ApiCreatedResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -32,14 +35,12 @@ export class UsersController { * * @param user User to create. * @returns The created user entity. - * @throws If the user creation failed. */ @Post() @ApiOperation({ description: "Create a new user." }) - @ApiCreatedResponse({ - type: UserEntity, - description: "Created. The user has been successfully created.", - }) + @ApiCreatedResponse({ type: UserEntity, description: "Created. The user has been successfully created." }) + @ApiConflictResponse({ description: "Conflict. Cannot update without corrupt the database." }) + @ApiBadRequestResponse({ description: "Bad Request. Invalid body content." }) async create(@Body() user: CreateUserDto): Promise { return this.usersService.create(user); } @@ -51,10 +52,7 @@ export class UsersController { */ @Get() @ApiOperation({ description: "Fetch all users." }) - @ApiOkResponse({ - type: [UserEntity], - description: "OK. The users have been successfully fetched.", - }) + @ApiOkResponse({ type: [UserEntity], description: "OK. The users have been successfully fetched." }) async findAll(): Promise { return this.usersService.findAll(); } @@ -64,15 +62,13 @@ export class UsersController { * * @param id ID of the user to fetch. * @returns The fetched user entity or null. - * @throws If the request failed. */ @Get(":id") @ApiOperation({ description: "Fetch one user." }) - @ApiOkResponse({ - type: UserEntity, - description: "OK. The user has been successfully fetched.", - }) - async findOne(@Param("id", ParseIntPipe) id: number): Promise { + @ApiOkResponse({ type: UserEntity, description: "OK. The user has been successfully fetched." }) + @ApiNotFoundResponse({ description: "Not Found. The user doesn't exist." }) + @ApiBadRequestResponse({ description: "Bad Request. Invalid id param." }) + async findOne(@Param("id", ParseIntPipe) id: number): Promise { return this.usersService.findOne(id); } @@ -82,14 +78,13 @@ export class UsersController { * @param id ID of the user to update. * @param user User data to update. * @returns The updated user entity. - * @throw If the update failed. */ @Patch(":id") @ApiOperation({ description: "Update one user." }) - @ApiOkResponse({ - type: UserEntity, - description: "OK. The user has been successfully updated.", - }) + @ApiOkResponse({ type: UserEntity, description: "OK. The user has been successfully updated." }) + @ApiNotFoundResponse({ description: "Not Found. The user to update doesn't exist." }) + @ApiConflictResponse({ description: "Conflict. Cannot update without corrupt the database." }) + @ApiBadRequestResponse({ description: "Bad Request. Invalid body content and/or id param." }) async update(@Param("id", ParseIntPipe) id: number, @Body() user: UpdateUserDto): Promise { return this.usersService.update(id, user); } @@ -99,14 +94,12 @@ export class UsersController { * * @param id ID of the user to delete. * @returns The deleted user entity. - * @throws If the deletion failed. */ @Delete(":id") @ApiOperation({ description: "Delete one user." }) - @ApiOkResponse({ - type: UserEntity, - description: "OK. The user has been successfully deleted.", - }) + @ApiOkResponse({ type: UserEntity, description: "OK. The user has been successfully deleted." }) + @ApiNotFoundResponse({ description: "Not Found. The user to delete doesn't exist." }) + @ApiBadRequestResponse({ description: "Bad Request. Invalid id param." }) remove(@Param("id", ParseIntPipe) id: number): Promise { return this.usersService.remove(id); } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 70b4472..eff926c 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -39,8 +39,8 @@ export class UsersService { * @returns The fetched user entity or null. * @throws If the query failed. */ - async findOne(id: number): Promise { - return this.prismaService.user.findUnique({ where: { id } }); + async findOne(id: number): Promise { + return this.prismaService.user.findUniqueOrThrow({ where: { id } }); } /** diff --git a/swagger-static/swagger-ui-init.js b/swagger-static/swagger-ui-init.js index a180b2c..08c8e3a 100644 --- a/swagger-static/swagger-ui-init.js +++ b/swagger-static/swagger-ui-init.js @@ -37,6 +37,12 @@ window.onload = function() { } } } + }, + "400": { + "description": "Bad Request. Invalid body content." + }, + "409": { + "description": "Conflict. Cannot update without corrupt the database." } }, "tags": [ @@ -93,6 +99,12 @@ window.onload = function() { } } } + }, + "400": { + "description": "Bad Request. Invalid id param." + }, + "404": { + "description": "Not Found. The user doesn't exist." } }, "tags": [ @@ -133,6 +145,15 @@ window.onload = function() { } } } + }, + "400": { + "description": "Bad Request. Invalid body content and/or id param." + }, + "404": { + "description": "Not Found. The user to update doesn't exist." + }, + "409": { + "description": "Conflict. Cannot update without corrupt the database." } }, "tags": [ @@ -163,6 +184,12 @@ window.onload = function() { } } } + }, + "400": { + "description": "Bad Request. Invalid id param." + }, + "404": { + "description": "Not Found. The user to delete doesn't exist." } }, "tags": [