Complete guide to module creation
This guide will teach you how to create a complete re-reference module from scratch, following the code style and best practices of your project. We will take the example of creating a "Product Management" module.
Table of contents
1. Create infrastructure using the Nest CLI
Step 1: Create a module
bash
# 在 src/modules/admin-api/system/ 目錄下創建新模塊
nest g module modules/admin-api/system/product
Step 2: Create a controller
bash
nest g controller modules/admin-api/system/product
Step 3: Create a Service
bash
nest g service modules/admin-api/system/product
Step 4: Create a directory structure
bash
# 創建必要的目錄
mkdir src/modules/admin-api/system/product/dto
mkdir src/modules/admin-api/system/product/entity
2. Create an Entity
File location:src/modules/admin-api/system/product/entity/product.entity.ts
typescript
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { BaseEntity } from '@/common/entities/base.entity'
import { CategoryEntity } from '@/modules/admin-api/system/category/entity/category.entity'
@Entity('system_product')
export class ProductEntity extends BaseEntity {
@PrimaryGeneratedColumn({ type: 'bigint', comment: '主鍵ID' })
id: string
@Column({ length: 100, comment: '產品名稱' })
name: string
@Column({ length: 50, comment: '產品代碼' })
code: string
@Column({ type: 'text', comment: '產品描述', nullable: true })
description: string
@Column({ type: 'decimal', precision: 10, scale: 2, comment: '產品價格' })
price: number
@Column({ type: 'int', comment: '庫存數量', default: 0 })
stock: number
@Column({ name: 'category_id', type: 'bigint', comment: '分類ID' })
categoryId: string
@Column({ length: 255, comment: '產品圖片', nullable: true })
image: string
@Column({ type: 'int', comment: '排序', default: 0 })
sort: number
// 關聯關係
@ManyToOne(() => CategoryEntity)
@JoinColumn({ name: 'category_id' })
category: CategoryEntity
}
Key points of physical design
- Inheritance BaseEntity: All entities should inherit
BaseEntity
To get the base bar - Primary key design: Use
bigint
Primary key of type - Column comments: Each column should have clear comments
- Affiliation relationship: Association decorator using TypeORM
- Data type: Choose the appropriate data type according to actual needs
3. Create a DTO file
Request DTO:src/modules/admin-api/system/product/dto/product.req.dto.ts
typescript
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'
import { Transform } from 'class-transformer'
import { IsDecimal, IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'
import { BaseReqDto, disableEditFields } from '@/common/dtos/base.req.dto'
import { ParseBigIntPipe } from '@/common/pipes/parse-bigInt-pipe'
import { EnvHelper } from '@/utils/env-helper'
class ProductReqDto extends BaseReqDto {
@ApiProperty({ description: '主鍵ID', required: true })
@IsNotEmpty()
@Transform(({ value }) => new ParseBigIntPipe().transform(value))
id: string
@ApiProperty({ description: '產品名稱', required: true, example: 'iPhone 15' })
@IsNotEmpty()
@IsString()
name: string
@ApiProperty({ description: '產品代碼', required: true, example: 'IPHONE15' })
@IsNotEmpty()
@IsString()
code: string
@ApiProperty({ description: '產品描述', example: '最新款iPhone' })
@IsOptional()
@IsString()
description: string
@ApiProperty({ description: '產品價格', example: 999.99 })
@IsNotEmpty()
@IsNumber()
price: number
@ApiProperty({ description: '庫存數量', example: 100 })
@IsNotEmpty()
@IsNumber()
stock: number
@ApiProperty({ description: '分類ID', example: '1' })
@IsNotEmpty()
@Transform(({ value }) => new ParseBigIntPipe().transform(value))
categoryId: string
@ApiProperty({ description: '產品圖片', example: 'https://example.com/image.jpg' })
@IsOptional()
@IsString()
image: string
@ApiProperty({ description: '排序', example: 1 })
@IsNotEmpty()
@IsNumber()
sort: number
}
// 創建產品 DTO
export class CreateProductReqDto extends PartialType(
OmitType(ProductReqDto, ['id', 'multilingualFields', ...disableEditFields]),
) {}
// 查詢產品 DTO
export class FindProductReqDto extends PartialType(ProductReqDto) {
@ApiProperty({ description: '分頁大小', example: 10, required: false })
@IsNotEmpty()
@Min(0)
@Max(EnvHelper.getNumber('MAX_PAGE_SIZE'))
pageSize?: number = 10
@ApiProperty({ description: '分頁頁碼', example: 1, required: false })
@IsNotEmpty()
@Min(0)
@Max(EnvHelper.getNumber('MAX_PAGE_NUMBER'))
currentPage?: number = 1
}
// 更新產品 DTO
export class UpdateProductReqDto extends PartialType(
OmitType(ProductReqDto, ['multilingualFields', ...disableEditFields]),
) {}
Response DTO:src/modules/admin-api/system/product/dto/product.res.dto.ts
typescript
import { PaginatedResponseDto, SingleResponseDto } from '@/utils/response-dto'
// 創建產品響應 DTO
export class CreateProductResDto extends SingleResponseDto({
id: '100',
}) {}
// 產品數據結構(用於 Swagger 文檔)
const ProductResDtoReturn = {
id: '1',
name: 'iPhone 15',
code: 'IPHONE15',
description: '最新款iPhone',
price: 999.99,
stock: 100,
categoryId: '1',
image: 'https://example.com/iphone15.jpg',
sort: 1,
category: {
id: '1',
name: '手機',
code: 'PHONE',
// 其他分類欄位...
},
remark: '備註',
status: 1,
isDeleted: 0,
creator: '1',
createTime: '2025-01-01T00:00:00.000Z',
updater: '1',
updateTime: '2025-01-01T00:00:00.000Z',
}
// 查詢產品列表響應 DTO
export class FindProductResDto extends PaginatedResponseDto(ProductResDtoReturn) {}
// 查詢單一產品響應 DTO
export class FindOneProductResDto extends SingleResponseDto(ProductResDtoReturn) {}
Key points of DTO design
- Inheritance BaseReqDto: Request DTO should inherit
BaseReqDto
- Use Decorators: Use
@ApiProperty
、@IsNotEmpty
Decorators - Data conversion: Use
@Transform
andParseBigIntPipe
Handle large integers - Pagination support: Query DTO contains paging parameters
- Swagger Documentation: Provides complete API documentation comments
4. Create a Service
File location:src/modules/admin-api/system/product/product.service.ts
typescript
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { _delete, create, find, findOne, update } from '@/common/services/base.service'
import { CreateProductReqDto, FindProductReqDto, UpdateProductReqDto } from './dto/product.req.dto'
import { ProductEntity } from './entity/product.entity'
@Injectable()
export class ProductService {
constructor(
@InjectRepository(ProductEntity)
private readonly productRepository: Repository<ProductEntity>,
) {}
// 新增產品
async create(createProductReqDto: CreateProductReqDto) {
const result = await create({
dto: createProductReqDto,
repository: this.productRepository,
repeatCondition: ['code'], // 產品代碼不能重複
modalName: '產品',
foreignKeyChecks: [
{
field: 'categoryId',
repository: this.productRepository.manager.getRepository('CategoryEntity'),
modalName: '分類',
},
],
})
return { id: result.id }
}
// 查詢產品
async find(findProductReqDto: FindProductReqDto) {
const { list, total } = await find({
dto: findProductReqDto,
repository: this.productRepository,
relations: ['category'], // 關聯查詢分類信息
where: {
isDeleted: 0,
},
})
// 處理敏感信息(如果有關聯用戶信息)
list.forEach((item) => {
if (item.category) {
// 可以對分類信息進行處理
delete item.category.remark // 移除備註等敏感信息
}
})
return {
total,
list,
}
}
// 查詢單一產品
async findOne(id: string) {
return await findOne({
id,
repository: this.productRepository,
relations: ['category']
})
}
// 更新產品
async update(updateProductReqDto: UpdateProductReqDto) {
await update({
dto: updateProductReqDto,
repository: this.productRepository,
existenceCondition: ['id'],
repeatCondition: ['code'], // 更新時檢查代碼重複
modalName: '產品',
})
}
// 刪除產品
async delete(id: string) {
await _delete({
id,
repository: this.productRepository,
modalName: '產品',
})
}
// 封鎖產品
async block(id: string) {
await update({
dto: { id, status: 0 },
repository: this.productRepository,
existenceCondition: ['id'],
modalName: '產品',
})
}
// 解封鎖產品
async unblock(id: string) {
await update({
dto: { id, status: 1 },
repository: this.productRepository,
existenceCondition: ['id'],
modalName: '產品',
})
}
}
Key points of service design
- Use BaseService: All CRUD operations are used
BaseService
Method - Repeatability Check: Use
repeatCondition
Check uniqueness - Foreign key verification: Use
foreignKeyChecks
Verify the associated data - Association Query: Use
relations
Conduct association query - Data processing: Process sensitive information before returning
5. Create a controller
File location:src/modules/admin-api/system/product/product.controller.ts
typescript
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseInterceptors } from '@nestjs/common'
import { ApiOperation, ApiResponse } from '@nestjs/swagger'
import { HasPermission } from '@/common/decorators/has-permission.decorator'
import { Operation, OperationType } from '@/common/decorators/operation.decorator'
import { ResponseMessage } from '@/common/decorators/response-message.decorator'
import { TransformInterceptor } from '@/common/interceptors/transform.interceptor'
import { ParseBigIntPipe } from '@/common/pipes/parse-bigInt-pipe'
import { MsgResponseDto } from '@/utils/response-dto'
import { CreateProductReqDto, FindProductReqDto, UpdateProductReqDto } from './dto/product.req.dto'
import { CreateProductResDto, FindOneProductResDto, FindProductResDto } from './dto/product.res.dto'
import { ProductService } from './product.service'
@Controller('/admin-api/system/product')
@UseInterceptors(TransformInterceptor)
export class ProductController {
constructor(private readonly productService: ProductService) {}
@Post('/create')
@HasPermission('system:product:create')
@Operation({ type: OperationType.CREATE, name: '建立產品', module: 'system-product' })
@ApiOperation({ summary: '建立產品' })
@ApiResponse({ type: CreateProductResDto })
@ResponseMessage('建立產品成功')
create(@Body() createProductReqDto: CreateProductReqDto) {
return this.productService.create(createProductReqDto)
}
@Get('/list')
@HasPermission('system:product:query')
@Operation({ type: OperationType.READ, name: '取得產品分頁列表', module: 'system-product' })
@ApiOperation({ summary: '取得產品分頁列表' })
@ApiResponse({ type: FindProductResDto })
@ResponseMessage('取得產品分頁列表成功')
find(@Query() findProductReqDto: FindProductReqDto) {
return this.productService.find(findProductReqDto)
}
@Get('/get/:id')
@HasPermission('system:product:query')
@Operation({ type: OperationType.READ, name: '獲取產品資料', module: 'system-product' })
@ApiOperation({ summary: '獲取產品資料' })
@ApiResponse({ type: FindOneProductResDto })
@ResponseMessage('獲取產品資料成功')
findOne(@Param('id', ParseBigIntPipe) id: string) {
return this.productService.findOne(id)
}
@Put('/update')
@HasPermission('system:product:update')
@Operation({ type: OperationType.UPDATE, name: '更新產品', module: 'system-product' })
@ApiOperation({ summary: '更新產品' })
@ApiResponse({ type: MsgResponseDto() })
@ResponseMessage('更新產品成功')
update(@Body() updateProductReqDto: UpdateProductReqDto) {
return this.productService.update(updateProductReqDto)
}
@Delete('/delete/:id')
@HasPermission('system:product:delete')
@Operation({ type: OperationType.DELETE, name: '刪除產品', module: 'system-product' })
@ApiOperation({ summary: '刪除產品' })
@ApiResponse({ type: MsgResponseDto() })
@ResponseMessage('刪除產品成功')
delete(@Param('id', ParseBigIntPipe) id: string) {
return this.productService.delete(id)
}
@Put('/block/:id')
@HasPermission('system:product:block')
@Operation({ type: OperationType.UPDATE, name: '封鎖產品', module: 'system-product' })
@ApiOperation({ summary: '封鎖產品' })
@ApiResponse({ type: MsgResponseDto() })
@ResponseMessage('封鎖產品成功')
block(@Param('id', ParseBigIntPipe) id: string) {
return this.productService.block(id)
}
@Put('/unblock/:id')
@HasPermission('system:product:unblock')
@Operation({ type: OperationType.UPDATE, name: '解封鎖產品', module: 'system-product' })
@ApiOperation({ summary: '解封鎖產品' })
@ApiResponse({ type: MsgResponseDto() })
@ResponseMessage('解封鎖產品成功')
unblock(@Param('id', ParseBigIntPipe) id: string) {
return this.productService.unblock(id)
}
}
Key points of controller design
- Routing Design: Routing using RESTful style
- Permission Control: Each interface uses
@HasPermission
Decorators - Operation record: Use
@Operation
Record operation logs - Swagger Documentation: Use
@ApiOperation
and@ApiResponse
Provide documentation - Parameter verification: Use
ParseBigIntPipe
Handle large integer parameters
6. Configuration module (Module)
File location:src/modules/admin-api/system/product/product.module.ts
typescript
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ProductEntity } from './entity/product.entity'
import { ProductController } from './product.controller'
import { ProductService } from './product.service'
@Module({
imports: [TypeOrmModule.forFeature([ProductEntity])],
controllers: [ProductController],
providers: [ProductService],
exports: [ProductService], // 如果其他模塊需要使用此服務
})
export class ProductModule {}
7. Register to the main module
Revisesrc/modules/admin-api/system/system.module.ts
typescript
import { Module } from '@nestjs/common'
// ... 其他導入
import { ProductModule } from './product/product.module'
@Module({
imports: [
// ... 其他模塊
ProductModule, // 添加新模塊
],
// ... 其他配置
})
export class SystemModule {}
8. Test module function
Test with Swagger
- Start the application
- access
http://localhost:3000/api-docs
- Find Product-related APIs
- Test each interface function
Test cases
typescript
// 測試創建產品
POST /admin-api/system/product/create
{
"name": "iPhone 15",
"code": "IPHONE15",
"description": "最新款iPhone",
"price": 999.99,
"stock": 100,
"categoryId": "1",
"image": "https://example.com/iphone15.jpg",
"sort": 1
}
// 測試查詢產品列表
GET /admin-api/system/product/list?pageSize=10¤tPage=1
// 測試查詢單一產品
GET /admin-api/system/product/get/1
// 測試更新產品
PUT /admin-api/system/product/update
{
"id": "1",
"name": "iPhone 15 Pro",
"price": 1099.99
}
// 測試刪除產品
DELETE /admin-api/system/product/delete/1
Advanced functions
1. Add multilingual support
If the product requires multilingual support, you can modify the entity:
typescript
// 在 ProductEntity 中添加多語言欄位
@Column({ length: 255, comment: '產品名稱(多語言)', nullable: true })
nameMultilingual: string
@Column({ length: 255, comment: '產品描述(多語言)', nullable: true })
descriptionMultilingual: string
2. Add soft delete check
Add soft delete checks to the service:
typescript
// 在查詢時過濾已刪除的記錄
const { list, total } = await find({
dto: findProductReqDto,
repository: this.productRepository,
where: {
isDeleted: 0, // 只查詢未刪除的記錄
},
})
3. Add business logic
typescript
// 添加庫存檢查
async checkStock(productId: string, quantity: number) {
const product = await this.findOne(productId)
if (product.stock < quantity) {
throw new BadRequestException('庫存不足')
}
return true
}
// 添加庫存更新
async updateStock(productId: string, quantity: number) {
await this.productRepository.update(
{ id: productId },
{ stock: () => `stock - ${quantity}` }
)
}
Frequently Asked Questions
1. Permission configuration
Make sure to configure the corresponding permissions in the permission system:
typescript
// 權限配置示例
const permissions = [
'system:product:create',
'system:product:query',
'system:product:update',
'system:product:delete',
'system:product:block',
'system:product:unblock',
]
2. Database migration
Create a database migration file:
bash
# 生成遷移文件
npm run migration:generate -- -n CreateProductTable
# 執行遷移
npm run migration:run
3. Error handling
Make sure all possible errors are handled properly:
typescript
try {
const result = await this.productService.create(dto)
return result
}
catch (error) {
if (error instanceof ConflictException) {
throw new BadRequestException('產品代碼已存在')
}
throw error
}
Summarize
Through the above steps, you have created a complete module for adding, deleting, modifying and checking. Remember the following points:
- Follow project style: Maintain consistency with existing code
- Use BaseService: Make full use of existing basic services
- Complete documentation: Provide clear API documentation
- Permission Control: Ensure proper permission control
- Error handling: Provide friendly error information
- Test: Fully test all functions
This module creation guide should help you quickly create new modules that meet project standards.