# Decorators for Endpoints
# For Controller
# Required Decorator
# ApiTags
@ApiTags("Public Collect Code")
# ApiOperation
@ApiOperation({
summary: "Request a collect code with X uon",
description: `
\nUser/Group requests a collect code with X uon real time via the Public UON API.
\nA \`distribution\` is created from the BE with the following fields:
\n - \`id\`: distribution id.
\n - \`maxTotalUons\`: maximum number of uons allowed for this distribution.
\n - \`salesType (RETAIL | WHOLESALE)\`: indicate UONs distributed from this distribution is RETAIL or WHOLESALE (for reporting purpose).
\n - \`userId\`: the user/group that is authorized for this distribution. Must be the same user that owns the "api-key" sent together in request header. Uon Card will have "sponsor by" value of this user/group's name.`,
})
# ApiSecurity
@ApiSecurity("api-key")
# ApiProperty
@ApiProperty({
type: Number,
required: true,
maximum: 2000,
minimum: 1,
example: 1,
description:
"The number of uons that the collect code to be generated would contain",
})
# Depends on API design (Stoplight), we need to add corresponding decorator
# ApiParam
@ApiParam({
name: "distributionId",
example: 1550858321152427,
description: "The `id` of the distribution",
type: "integer",
required: true,
format: "int64",
})
# ApiQuery
@ApiQuery({
name: "offset",
example: 1,
description: "pagination",
type: "number",
required: true,
})
# ApiOkResponse
class PublicCollectCodeCreateResponseData {
@ApiProperty({
example: "WT0005-AMSHWYQB",
required: true,
type: String,
})
collectCode!: string;
@ApiProperty({
example: "https://earthtoday.com/collect/john?code=WT0005-AMSHWYQB",
required: true,
type: String,
})
collectUrl!: string;
}
class PublicCollectCodeCreateResponseDto {
@ApiProperty({
required: true,
type: PublicCollectCodeCreateResponseData,
title: "",
})
data!: PublicCollectCodeCreateResponseData;
}
@ApiOkResponse({
description: "The collect code has been successfully created.",
type: PublicCollectCodeCreateResponseDto,
})
Notice that title
property from PublicCollectCodeCreateResponseDto
is empty string, this make Redoc render cleaner as image below
# ApiNotFoundResponse
export abstract class NotFoundErrorApiProperties {
@ApiProperty({
example: "ERROR",
})
status!: string;
@ApiProperty({
example: "NotFoundError",
})
type!: string;
@ApiProperty({
example: "The resource cannot be found in the system",
})
detail!: string;
@ApiProperty({
example: "53bffb246fb16f60613abf31a95102a3",
})
correlationId!: string;
@ApiProperty({
example: "<ResourceType>",
type: "string",
enum: NotFoundErrorResourceType,
})
resourceType!: NotFoundErrorResourceType;
}
@ApiNotFoundResponse({
description:
"The distribution with the provided id cannot be found in the system",
type: NotFoundErrorApiProperties,
})
# ApiBadRequestResponse
@ApiBadRequestResponse({
description: "You have an error in your request",
type: BadRequestErrorApiProperties,
})
# ApiForbiddenResponse
@ApiForbiddenResponse({
description:
"The maximum number of UONs authorized for this distribution has been exceeded",
type: MaximumNumberOfUONsExceededErrorApiProperties,
})
# ApiUnauthorizedResponse
@ApiUnauthorizedResponse({
description: "The provided api-key is invalid",
type: AuthorizationErrorApiProperties,
})
# ApiInternalServerErrorResponse
@ApiInternalServerErrorResponse({
description:
"The server encountered an unexpected condition that prevented it from fulfilling the request",
type: InternalServerErrorApiProperties,
})
# For API request body, response
Every request body, response MUST be defined as a class, and the name convention is XxxPayloadDto
& XxxResponseDto
class PublicCollectCodeCreateDataPayload {
@ApiProperty({
type: Number,
required: true,
maximum: 2000,
minimum: 1,
example: 1,
description:
"The number of uons that the collect code to be generated would contain",
})
@IsNumber()
@Max(2000, {
message: "Maximum uon amount is 2000",
})
@Min(1, {
message: "Minimum uon amount is 1",
})
uonAmount!: number;
}
export class PublicCollectCodeCreatePayloadDto {
@ValidateNested()
@ApiProperty({ type: PublicCollectCodeCreateDataPayload, title: "" })
@Type(() => PublicCollectCodeCreateDataPayload)
@IsNotEmpty()
data!: PublicCollectCodeCreateDataPayload;
}
export class PublicCollectCodeCreateResponseDto {
...
}
Most of the cases, ApiProperty
decorator is enough.
Notice that the request body and response ALWAYS wrapped with data
property
# For API error response
We can define error response schema as below
@ApiUnauthorizedResponse({
description: "The provided api-key is invalid",
schema: {
type: "object",
properties: {
status: {
type: "string",
example: "ERROR",
},
type: {
type: "string",
example: "AuthorizationError",
},
detail: {
type: "string",
example: "The provided api-key is invalid",
},
correlationId: {
type: "string",
example: "53bffb246fb16f60613abf31a95102a3",
},
},
},
})
But we SHOULD define an abstract class for reusable, this class usually exported from the same custom error file
AuthorizationError.ts
export abstract class AuthorizationErrorApiProperties {
...
}
publicCollectCode.controller.ts
@ApiUnauthorizedResponse({
description: "The provided api-key is invalid",
type: AuthorizationErrorApiProperties,
})
# Utility Decorator
# PersonalRequestDecorator
Endpoints with format /v*/users/me/...
we can apply PersonalRequestDecorator as shortcut to apply multiple decorators at a same time
@Get("/users/me/cards/starred")
@PersonalRequestDecorator()
Body structure of PersonalRequestDecorator looks like:
export function PersonalRequestDecorator() {
return applyDecorators(
ApiBearerAuth(),
ApiUnauthorizedResponse({
description: "The bearer token is invalid",
type: AuthenticationFailedErrorApiProperties,
}),
);
}
# PublicApiRequestDecorator
Endpoints with format /public/v*/users/me/...
we can apply PublicApiRequestDecorator as shortcut to apply multiple decorators at a same time
@Post("public/v1/distributions/:distributionId/collect-codes")
@PublicApiRequestDecorator()
Body structure of PublicApiRequestDecorator looks like:
export function PublicApiRequestDecorator() {
return applyDecorators(
ApiSecurity("api-key"),
ApiUnauthorizedResponse({
description: "The provided api-key is invalid",
type: AuthorizationErrorApiProperties,
}),
ApiInternalServerErrorResponse({
description:
"The server encountered an unexpected condition that prevented it from fulfilling the request",
type: InternalServerErrorApiProperties,
}),
);
}