Skip to content

Commit

Permalink
feat: handle errors with prisma exception filter
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasBuecher committed Jan 4, 2023
1 parent 287d4b2 commit 8bb926e
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 28 deletions.
9 changes: 8 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PrismaClientExceptionFilter } from "./prisma-client-exception.filter";

describe("PrismaClientExceptionFilter", () => {
it("should be defined", () => {
expect(new PrismaClientExceptionFilter()).toBeDefined();
});
});
36 changes: 36 additions & 0 deletions src/prisma-client-exception/prisma-client-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
43 changes: 18 additions & 25 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
ParseIntPipe,
} from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiConflictResponse,
ApiCreatedResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
Expand All @@ -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<UserEntity> {
return this.usersService.create(user);
}
Expand All @@ -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<UserEntity[]> {
return this.usersService.findAll();
}
Expand All @@ -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<UserEntity | null> {
@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<UserEntity> {
return this.usersService.findOne(id);
}

Expand All @@ -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<UserEntity> {
return this.usersService.update(id, user);
}
Expand All @@ -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<UserEntity> {
return this.usersService.remove(id);
}
Expand Down
4 changes: 2 additions & 2 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export class UsersService {
* @returns The fetched user entity or null.
* @throws If the query failed.
*/
async findOne(id: number): Promise<UserEntity | null> {
return this.prismaService.user.findUnique({ where: { id } });
async findOne(id: number): Promise<UserEntity> {
return this.prismaService.user.findUniqueOrThrow({ where: { id } });
}

/**
Expand Down
27 changes: 27 additions & 0 deletions swagger-static/swagger-ui-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ window.onload = function() {
}
}
}
},
"400": {
"description": "Bad Request. Invalid body content."
},
"409": {
"description": "Conflict. Cannot update without corrupt the database."
}
},
"tags": [
Expand Down Expand Up @@ -93,6 +99,12 @@ window.onload = function() {
}
}
}
},
"400": {
"description": "Bad Request. Invalid id param."
},
"404": {
"description": "Not Found. The user doesn't exist."
}
},
"tags": [
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down

1 comment on commit 8bb926e

@vercel
Copy link

@vercel vercel bot commented on 8bb926e Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.