I'm working on a little NestJS project to create and manage events and tickets.
So, I'm using UUIDs in request parameter of my controller like this to get, update and delete my entities in my database:
@Get(':eventId')
async findOne(
@UserId() userId: string,
@Param('eventId') eventId: string,
): Promise<EventEntity> {
return this.eventService.findOne(userId, eventId);
}
Until this point, there is no issue.
Then, I implemented a custom decorator to validate the UUID format. Here is the code snippet:
export const IsUUIDParam = createParamDecorator(
(data: string, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
const uuid: string = request.params[data];
if (!uuid) {
return uuid;
}
// This regex checks if the string is a valid UUIDv4
const cmp: RegExpMatchArray = uuid.match(
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
);
if (cmp === null) {
throw new BadRequestException(`Invalid ${data} format`);
}
return uuid;
},
);
And I use the new decorator like this in my controller:
@Get(':eventId')
async findOne(
@UserId() userId: string,
@IsUUIDParam('eventId') eventId: string,
): Promise<EventEntity> {
return this.eventService.findOne(userId, eventId);
}
The custom decorator works well, but right now, the the Swagger does not display the required parameter.
Screenshot of Swagger not displaying the required parameter
So I followed this Stack Overflow post to implement the documentation on my custom decorator.
Here is my new custom decorator:
export const IsUUIDParam = createParamDecorator(
(data: string, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
const uuid: string = request.params[data];
if (!uuid) {
return uuid;
}
// This regex checks if the string is a valid UUIDv4
const cmp: RegExpMatchArray = uuid.match(
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
);
if (cmp === null) {
throw new BadRequestException(`Invalid ${data} format`);
}
return uuid;
},
[
(target, key): void => {
// The code below define the Swagger documentation for the custom decorator
const explicit =
Reflect.getMetadata(DECORATORS.API_PARAMETERS, target[key]) ?? [];
Reflect.defineMetadata(
DECORATORS.API_PARAMETERS,
[
...explicit,
{
in: 'path',
name: 'uuid',
required: true,
type: 'string',
},
],
target[key],
);
},
],
);
But now, the Swagger documentation only display uuid
:
Screenshot of Swagger displaying uuid instead of the required parameter
But I want to display eventId
or the name of the parameter in a generic way (for example ticketId
somewhere else in another controller).
I tried to get something from the target
and key
properties, but I didn't find anything.
I didn't find anything neither on Internet nor with ChatGPT, and the data
property is not accessible in the second parameter of the createParamDecorator()
method where I'm trying to custom the Swagger documentation.
Do you know how can I fix my issue?
I finally found the answer.
We can fix this issue by encapsulating the createParamDecorator()
function in an arrow function taking a string parameter (here the data
parameter).
export const IsUUIDParam = (data: string) => // new arrow function that takes a string
createParamDecorator(
// the object _ allow us to declare to our IDE that the parameter won't be used, and so it doesn't display a warning message
(_: string, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
const uuid: string = request.params[data];
if (!uuid) {
return uuid;
}
// This regex checks if the string is a valid UUIDv4
const cmp: RegExpMatchArray = uuid.match(
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
);
if (cmp === null) {
throw new BadRequestException(`Invalid ${data} format`);
}
return uuid;
},
[
(target, key): void => {
// The code below define the Swagger documentation for the custom decorator
const explicit =
Reflect.getMetadata(DECORATORS.API_PARAMETERS, target[key]) ?? [];
Reflect.defineMetadata(
DECORATORS.API_PARAMETERS,
[
...explicit,
{
in: 'path',
// use the new data object here
name: data,
required: true,
type: 'string',
},
],
target[key],
);
},
],
// Do not forget to add the parenthesis at the end to execute the arrow function
)();
Thanks to that, the data
object (containing the UUIDv4 string) is accessible everywhere in the arrow function and so, in the second part of the createParamDecorator()
function.
We can change the first argument of the createParamDecorator()
function by an underscore (_
) to avoid warning messages in our IDE as we don't use this parameter anymore.
We can then update the name
property in our decorator with data
to display the given name (in a dynamic way).
Finally, add parenthesis at the end of the arrow function to execute it (()
)
Nothing changes in the controller, we can still call our custom decorator with the following code :
@Get(':eventId')
async findOne(
@UserId() userId: string,
@IsUUIDParam('eventId') eventId: string,
): Promise<EventEntity> {
return this.eventService.findOne(userId, eventId);
}
Here is a screenshot of the Swagger result for the event entity:
Screenshot of the Swagger result - GET /event/:eventId
And for the ticket entity:
Screenshot of the Swagger result - GET /ticket/:ticketId
Note: if you want to customize even more the decorator (oto add a custom description for example), you can add a parameter after data
in the function prototype, give a second parameter when calling the function in the controller and finally use the new parameter wherever you want in the arrow function.
And voilĂ ! We can display the string parameter of our custom decorator in a dynamic way in Swagger.