usecase-regask
Best practices for creating NestJS usecases in Regask, including structure, error handling
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Best practices for creating NestJS usecases in Regask, including structure, error handling
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 routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing.
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 | usecase-regask |
| description | Best practices for creating NestJS usecases in Regask, including structure, error handling |
types.ts file using namespaceshandle() method, organizing code with private methods by concern (e.g. guard checks, business logic, side effects).Every usecase must:
BaseUseCase<Input, Output>handle() method with private methods by concernUseCaseResult<Output> with structured error handlingimport { Injectable } from '@nestjs/common';
import { Logger } from '@regask/common-lib/logger/logger.service';
import { BaseUseCase, UseCaseResult } from '@regask/common-lib/workflow';
import {
BusinessRuleException,
NotFoundException,
PermissionDeniedException,
} from '@regask/common-lib/workflow/types';
import { MyUseCaseTypes } from './types';
type Input = MyUseCaseTypes.Input;
type Output = MyUseCaseTypes.Output;
@Injectable()
export class MyUseCase extends BaseUseCase<Input, Output> {
constructor(
private myRepository: MyRepository,
private myConcern: MyConcern,
) {
super();
this.logger = new Logger(MyUseCase.name);
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
// 1. Guard checks (validation, authorization)
await this.#guardChecks(input);
// 2. Main business logic
const result = await this.#executeBusinessLogic(input);
// 3. Side effects (events, notifications)
await this.#publishEvents(result);
return {
data: result,
error: null,
};
}
async #guardChecks(input: Input): Promise<void> {
// Validation and authorization logic
}
async #executeBusinessLogic(input: Input): Promise<Output> {
// Core business logic
}
async #publishEvents(data: Output): Promise<void> {
// Event publishing / side effects
}
}
Define types in a separate types.ts file using namespaces:
import { AuditUser } from '@regask/common-lib/microservices/identity/types/audit-user.type';
import { MyEntity } from '@/cores/entities/my-entity/entity';
export namespace MyUseCaseTypes {
export type Input = {
id: string; // Required fields first
userId: string;
companyId: string;
remarks?: string; // Optional fields with ?
reviewer?: AuditUser; // Use AuditUser for user context
};
export type Output = MyEntity;
}
Important: Use AuditUser for User Context
For actors like reviewer, approver, submitter, or any user performing an action, always use AuditUser from common-lib instead of creating custom types. AuditUser is the standard user context type across the system and includes:
userId, userCompany, userRolefirstName, lastName, emailpermission (RBAC permissions)// GOOD: Use AuditUser for user actors
export type Input = {
id: string;
reviewer: AuditUser; // Standard user context
};
// BAD: Don't create custom user types for actors
export type Input = {
id: string;
reviewer: { // Avoid custom actor types
id: string;
name: string;
};
};
For complex inputs with pagination/sorting:
export namespace FetchDataGridUseCaseTypes {
export type Sort = {
sortBy?: string;
sortDirection?: 'asc' | 'desc';
};
export type Pagination = {
page?: number;
limit?: number;
};
export type Input = {
companyId: string;
userId: string;
filter?: GridFilterModel;
searchKey?: string;
pagination?: Pagination;
sorts?: Sort[];
};
export type Output = {
total: number;
items: MyEntity[];
};
}
Organize code in handle() with clear separation:
async handle(input: Input): Promise<UseCaseResult<Output>> {
this.logger.info(`Executing usecase ${MyUseCase.name}`, { input });
// 1. Build filters and search criteria
const searchFilter = this.#buildSearchFilter(input);
// 2. Parallelize independent operations
const [total, items] = await Promise.all([
this.#getFilteredRecordCount(searchFilter),
this.#executeSearchQuery(input, searchFilter),
]);
// 3. Return result
return {
error: null,
data: { total, items },
};
}
Use specific exception types with error codes:
// Not found
throw new NotFoundException('entity_not_found').withErrorCode('ENTITY_NOT_FOUND');
// Business rule violation
throw new BusinessRuleException('entity_not_pending').withErrorCode('ENTITY_NOT_PENDING');
// Permission denied
throw new PermissionDeniedException('user_not_allowed').withErrorCode('USER_NOT_ALLOWED');
Extract logic into concerns when:
Place concerns in:
workflows/shared/concerns/ - for cross-workflow concernsworkflows/{workflow-name}/concerns/ - for workflow-specific concernsimport { Injectable } from '@nestjs/common';
import { PermissionDeniedException } from '@regask/common-lib/workflow/types';
@Injectable()
export class RbacPermissionConcern {
guardApproval(reviewer: AuditUser, purpose: PurposeEnum): void {
if (!this.#hasPermissionForPurpose(reviewer, purpose)) {
throw new PermissionDeniedException(
'user_not_allowed_to_approve',
).withErrorCode('USER_NOT_ALLOWED_TO_APPROVE');
}
}
#hasPermissionForPurpose(reviewer: AuditUser, purpose: PurposeEnum): boolean {
const permissionMapping = PURPOSE_PERMISSION_MAP[purpose];
if (!permissionMapping) return false;
const { module, action } = permissionMapping;
return reviewer.permission?.[module]?.[action] === true;
}
}
@Injectable()
export class ApproveUseCase extends BaseUseCase<Input, Output> {
constructor(
private rbacPermissionConcern: RbacPermissionConcern,
) {
super();
}
async #guardCheckApproval(input: Input): Promise<void> {
this.rbacPermissionConcern.guardApproval(
input.reviewer,
input.purpose,
);
}
}
When a usecase does too many things:
// Instead of one large usecase doing everything:
// ProcessOrderUseCase (validates, creates, notifies, updates inventory)
// Break into:
// ValidateOrderUseCase
// CreateOrderUseCase
// NotifyOrderCreatedUseCase
// UpdateInventoryUseCase
// Orchestrate if needed:
@Injectable()
export class ProcessOrderUseCase extends BaseUseCase<Input, Output> {
constructor(
private validateOrderUseCase: ValidateOrderUseCase,
private createOrderUseCase: CreateOrderUseCase,
private notifyOrderCreatedUseCase: NotifyOrderCreatedUseCase,
) {
super();
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
await this.validateOrderUseCase.executeOrThrowHttpError(input);
const order = await this.createOrderUseCase.executeOrThrowHttpError(input);
await this.notifyOrderCreatedUseCase.execute({ order });
return { data: order, error: null };
}
}
import { CustomTestHelper } from '@/test-helper/custom.test-helper';
import { AppModule } from '@/app.module';
import { CONNECTION_NAME } from '@/infra/database/constant';
describe('@workflows/my-workflow/my-usecase', () => {
const testHelper = new CustomTestHelper(CONNECTION_NAME, AppModule);
beforeAll(async () => {
await testHelper.beforeAll();
});
afterAll(() => testHelper.afterAll());
afterEach(() => testHelper.cleanUp());
});
ALWAYS use factoryBuilder for database interactions. NEVER use toBeDefined() for assertions.
// Create single entity
const entity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
// Create multiple entities
await testHelper
.factoryBuilder(MyEntityFactory)
.createMany(3, {
companyId,
});
describe('when entity exists and user has permission', () => {
it('should update status to Approved', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
const { error, data } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer,
});
expect(error).toBeNull();
expect(data.status).toBe(StatusEnum.Approved);
expect(data.review.reviewer.id).toBe(reviewer.userId);
});
});
describe('when entity does not exist', () => {
it('should return error code ENTITY_NOT_FOUND', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const nonExistentId = new mongoose.Types.ObjectId().toString();
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: nonExistentId,
reviewer,
});
expect(error.type).toBe(UseCaseErrorType.NOT_FOUND);
expect(error.data.errorCode).toBe('entity_not_found');
});
});
describe('when entity is not pending', () => {
it('should return error code ENTITY_NOT_PENDING', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const approvedEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Approved,
});
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: approvedEntity._id.toString(),
reviewer,
});
expect(error.type).toBe(UseCaseErrorType.BUSINESS_RULE);
expect(error.data.errorCode).toBe('entity_not_pending');
});
});
describe('when user does not have permission', () => {
it('should return error code USER_NOT_ALLOWED', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewerWithoutPermission = AuditUserFactory.create({
userCompany: companyId,
permission: {
[ClientModuleEnum.CLIENT__ALERT]: {
[ClientAlertActionEnum.APPROVE_ALERTS]: false,
},
} as PermissionValueObject,
});
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer: reviewerWithoutPermission,
});
expect(error.type).toBe(UseCaseErrorType.PERMISSION_DENIED);
expect(error.data.errorCode).toBe('user_not_allowed');
});
});
describe('when signaling event', () => {
it('should send Kafka message', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const entityId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
entityId,
status: StatusEnum.Pending,
});
const sendKafkaMessageSpy = jest.spyOn(
testHelper.get(KafkaProducerV2Service),
'sendMessage',
);
const { error, data } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer,
});
expect(error).toBeNull();
expect(sendKafkaMessageSpy).toHaveBeenCalledTimes(1);
expect(sendKafkaMessageSpy).toHaveBeenCalledWith(
EventTopicEnumV2.MyEventTopic,
{
eventBody: {
entityId: data._id.toString(),
companyId: entityId,
},
eventMetadata: {},
},
);
});
});
import { faker } from '@faker-js/faker';
import mongoose from 'mongoose';
import { MyEntity } from '@/cores/entities/my-entity/entity';
import { StatusEnum } from '@/cores/entities/my-entity/enums';
import { MyRepository } from '@/cores/repositories/my-entity/repository';
import { InjectRepository } from '../decorators/inject-repository.decorator';
@InjectRepository(MyRepository)
export class MyEntityFactory {
static create(override?: Partial<MyEntity>): MyEntity {
const entity: MyEntity = {
_id: new mongoose.Types.ObjectId(),
displayId: `MY${faker.string.numeric(2)}`,
companyId: new mongoose.Types.ObjectId().toString(),
status: faker.helpers.enumValue(StatusEnum),
createdBy: {
id: new mongoose.Types.ObjectId().toString(),
name: faker.person.fullName(),
},
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
...override,
};
return entity;
}
static createMany(num: number, override?: Partial<MyEntity>): MyEntity[] {
return Array.from({ length: num }, () => this.create(override));
}
}
Create reusable helper functions for common test setups:
const createUserWithPermission = (
overrides: Partial<AuditUser> = {},
): AuditUser =>
AuditUserFactory.create({
...overrides,
permission: {
[ClientModuleEnum.CLIENT__ALERT]: {
[ClientAlertActionEnum.APPROVE_ALERTS]: true,
},
} as PermissionValueObject,
});