router-regask
Best practices for creating NestJS routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Best practices for creating NestJS routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Skill for creating and managing repositories in NestJS with MongoDB, following best practices and
Best practices for creating NestJS usecases in Regask, including structure, error handling
Backend engineering specialist for APIs, databases, system architecture, and server-side development
Frontend development specialist for UI/UX, frameworks, responsive design, and web application architecture
Solution architecture reviewer for design patterns, scalability, maintainability, and hexagonal architecture
Technology research and evaluation specialist for comparing frameworks, libraries, and adoption strategies
| name | router-regask |
| description | Best practices for creating NestJS routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing. |
@GetAuditUser, @CompanyFeatureFlag, @RbacPermissions) or create new ones if needed@ApiTags, @ApiOperation, and response decorators (e.g. @ApiOkResponse, @ApiBadRequestResponse)Every controller must:
@Controller, @ApiTags, @ApiOperation)executeOrThrowHttpErrorimport { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { GetAuditUser } from '@regask/common-lib/decorators';
import { AuditUser } from '@regask/common-lib/microservices/identity/types/audit-user.type';
import { CompanyFeatureFlag } from '@regask/common-lib/decorators/company-feature-flag.decorator';
import { FeatureFlagEnum } from '@regask/common-lib/enums';
import { Routes, RouteVersion } from '@/routers/routes';
import { MyUseCase } from '@/workflows/my-workflow/usecase';
import { MyRequestDto } from './request.dto';
import { MyResponseDto } from './response.dto';
import { ResponseMapper } from './mapper';
@Controller({
version: [RouteVersion.v2],
path: Routes.myResource.action,
})
@ApiTags('My Resource')
export class MyController {
constructor(private readonly myUseCase: MyUseCase) {}
@Post()
@ApiOkResponse({ type: MyResponseDto })
@ApiOperation({ summary: 'Perform action on resource' })
@CompanyFeatureFlag(FeatureFlagEnum.myFeatureFlag)
@HttpCode(HttpStatus.OK)
async post(
@GetAuditUser() user: AuditUser,
@Param('id') id: string,
@Body() body: MyRequestDto,
): Promise<MyResponseDto> {
const data = await this.myUseCase.executeOrThrowHttpError({
id,
reviewer: user,
remarks: body.remarks,
});
return ResponseMapper.toMyResponseDto(data);
}
}
Use executeOrThrowHttpError when you want automatic HTTP error handling:
async post(
@GetAuditUser() user: AuditUser,
@Param('id') id: string,
@Body() body: MyRequestDto,
): Promise<MyResponseDto> {
const data = await this.myUseCase.executeOrThrowHttpError({
id,
reviewer: user,
remarks: body.remarks,
});
return ResponseMapper.toMyResponseDto(data);
}
Use this pattern when you need to access both data and error:
import { throwIfHaveErrorInUseCase } from '@regask/common-lib/controllers/usecaseErrorHandler';
async post(
@GetAuditUser() user: AuditUser,
@Body() body: MyRequestDto,
): Promise<MyResponseDto> {
const { error, data } = await this.myUseCase.execute({
companyId: user.userCompany,
userId: user.userId,
filter: body.filter,
pagination: body.pagination,
});
throwIfHaveErrorInUseCase(error);
return {
total: data.total,
items: mapToDataGridItems(data.items),
};
}
| UseCaseErrorType | HTTP Status |
|---|---|
| INVALID_INPUT | 400 Bad Request |
| NOT_FOUND | 404 Not Found |
| CONFLICT | 409 Conflict |
| BUSINESS_RULE | 422 Unprocessable Entity |
| PERMISSION_DENIED | 403 Forbidden |
| INTERNAL_SERVER_ERROR | 500 Internal Server Error |
Use mappers when:
import { MyEntity } from '@/cores/entities/my-entity/entity';
import { MyResponseDto } from './response.dto';
export class ResponseMapper {
static toMyResponseDto(data: MyEntity): MyResponseDto {
return {
id: data._id.toString(), // Convert ObjectId
displayId: data.displayId,
status: data.approvalStatus, // Rename field
type: data.entity,
review: data.review // Conditional mapping
? {
reviewer: {
id: data.review.reviewer.id,
name: data.review.reviewer.name,
},
reviewedAt: data.review.reviewedAt,
action: data.review.action,
remarks: data.review.remarks,
}
: undefined,
};
}
// Reuse mappers for similar responses
static toAnotherResponseDto(data: MyEntity): AnotherResponseDto {
return this.toMyResponseDto(data);
}
}
export function mapToDataGridItems(
items: MyEntity[],
): DataGridItem[] {
return items.map((item) => ({
id: item._id.toString(),
title: item.entityTitle,
status: item.approvalStatus,
lastSubmittedBy: {
id: item.updatedBy.id,
name: item.updatedBy.name,
},
lastSubmittedAt: item.updatedAt,
}));
}
For operations that don't return data:
@Controller({
version: [RouteVersion.v2],
})
@ApiTags('My Resource')
export class UpdateController {
constructor(private readonly updateUseCase: UpdateUseCase) {}
@Patch(Routes.myResource.update)
@HttpCode(HttpStatus.NO_CONTENT) // Returns 204
@ApiOperation({ summary: 'Update resource' })
@ApiResponse({
status: HttpStatus.NO_CONTENT,
description: 'Resource updated successfully',
})
async update(
@Body() body: UpdateRequestDto,
): Promise<void> { // No return data
const { error } = await this.updateUseCase.execute({
resourceId: body.resourceId,
data: body.data,
});
throwIfHaveErrorInUseCase(error);
}
}
import { CustomTestHelper } from '@/test-helper/custom.test-helper';
import { AppModule } from '@/app.module';
import { CONNECTION_NAME } from '@/infra/database/constant';
import { v2Prefix } from '@/routers/routes';
describe('@routers/v2/my-resource/action', () => {
const testHelper = new CustomTestHelper(CONNECTION_NAME, AppModule);
beforeAll(async () => {
await testHelper.beforeAll();
});
afterAll(() => testHelper.afterAll());
});
it('Should return 200 with response data', async () => {
const resourceId = new mongoose.Types.ObjectId().toString();
const authorizedUser = AuditUserFactory.create({
userRole: Role.CLIENTADMIN,
});
const entity = MyEntityFactory.create({
_id: new mongoose.Types.ObjectId(resourceId),
companyId: authorizedUser.userCompany,
status: StatusEnum.Approved,
});
const url = `${v2Prefix}/${Routes.myResource.action.replace(':id', resourceId)}`;
const res = await testHelper.request
.post(url)
.send({ remarks: 'Test remarks' })
.requestedBy(authorizedUser) // Set user context
.withFeatureFlags(FeatureFlagEnum.myFeature) // Enable feature flag
.executedWithUsecase({
usecase: MyUseCase,
expectedInput: { // Verify input
id: resourceId,
reviewer: authorizedUser,
remarks: 'Test remarks',
},
mockOutput: { // Mock response
data: entity,
error: null,
},
})
.expect(200);
expect(res.body.id).toBe(entity._id.toString());
expect(res.body.status).toBe(StatusEnum.Approved);
});
it('Should return 422 if usecase returns business error', async () => {
const resourceId = new mongoose.Types.ObjectId().toString();
const authorizedUser = AuditUserFactory.create({
userRole: Role.CLIENTADMIN,
});
const url = `${v2Prefix}/${Routes.myResource.action.replace(':id', resourceId)}`;
const res = await testHelper.request
.post(url)
.send({})
.requestedBy(authorizedUser)
.withFeatureFlags(FeatureFlagEnum.myFeature)
.executedWithUsecase({
usecase: MyUseCase,
expectedInput: {
id: resourceId,
reviewer: authorizedUser,
remarks: undefined,
},
mockOutput: {
data: null,
error: new UseCaseError(
'entity_not_pending',
UseCaseErrorType.BUSINESS_RULE,
),
},
})
.expect(422);
expect(res.body.message).toBe('entity_not_pending');
});
it('Should return 404 if entity not found', async () => {
const resourceId = new mongoose.Types.ObjectId().toString();
const authorizedUser = AuditUserFactory.create();
const url = `${v2Prefix}/${Routes.myResource.action.replace(':id', resourceId)}`;
const res = await testHelper.request
.post(url)
.send({})
.requestedBy(authorizedUser)
.withFeatureFlags(FeatureFlagEnum.myFeature)
.executedWithUsecase({
usecase: MyUseCase,
expectedInput: {
id: resourceId,
reviewer: authorizedUser,
remarks: undefined,
},
mockOutput: {
data: null,
error: new UseCaseError(
'Entity not found',
UseCaseErrorType.NOT_FOUND,
),
},
})
.expect(404);
});
it('Should return 400 if required field is missing', async () => {
const resourceId = new mongoose.Types.ObjectId().toString();
const authorizedUser = AuditUserFactory.create({
userRole: Role.CLIENTADMIN,
});
const url = `${v2Prefix}/${Routes.myResource.action.replace(':id', resourceId)}`;
const res = await testHelper.request
.post(url)
.send({}) // Missing required field
.requestedBy(authorizedUser)
.withFeatureFlags(FeatureFlagEnum.myFeature)
.expect(400);
expect(res.body.message).toContain('remarks should not be empty');
});
describe('When usecase executes successfully', () => {
it('Should return 204 No Content', async () => {
const body = {
resourceId: 'some-resource-id',
data: ['item1', 'item2'],
};
await testHelper.request
.patch(UpdateUrl)
.send(body)
.requestedBy(authorizedUser)
.executedWithUsecase({
usecase: UpdateUseCase,
expectedInput: {
resourceId: body.resourceId,
data: body.data,
},
mockOutput: {
data: null, // No data for 204
error: null,
},
})
.expect(204);
});
});
| Method | Purpose |
|---|---|
.requestedBy(user) | Set AuditUser in request headers |
.withFeatureFlags(...flags) | Enable feature flags for request |
.executedWithUsecase({...}) | Spy on usecase, verify input, mock output |
.expect(statusCode) | Assert HTTP response status |
async post(
@GetAuditUser() user: AuditUser,
@Body() body: BulkApproveRequestDto,
): Promise<BulkApprovedResponseDto> {
const { approvalIds, remarks } = body;
const { approvedList, approvedCount, totalCount } =
await this.bulkApproveUseCase.executeOrThrowHttpError({
approvalIds,
reviewer: user,
remarks,
});
return {
totalCount,
approvedCount,
items: approvedList.map((item) =>
ResponseMapper.toMyResponseDto(item),
),
};
}
import { CompanyFeatureFlag } from '@regask/common-lib/decorators/company-feature-flag.decorator';
import { FeatureFlagEnum } from '@regask/common-lib/enums';
@Controller({
version: [RouteVersion.v2],
path: Routes.myResource.action,
})
export class MyController {
@Post()
@CompanyFeatureFlag(FeatureFlagEnum.myFeatureFlag) // Guard with feature flag
async post(...): Promise<MyResponseDto> {
// Only accessible when feature flag is enabled
}
}