小满nestjs(第二十七章 实战:从零构建一个带分页与搜索的NestJS CRUD后台)
1. 从零搭建NestJS CRUD后台系统最近在做一个后台管理系统发现很多新手朋友对如何实现一个完整的CRUD功能比较困惑。今天我就用最接地气的方式手把手带你实现一个带分页和搜索的NestJS后台系统。这个系统会包含用户管理模块实现增删改查全套功能还会加入数据校验和模糊搜索这些实用特性。先说说为什么选择NestJS。作为一个基于Node.js的后端框架NestJS完美结合了面向对象编程和函数式编程的优点。它内置的依赖注入、模块化设计让代码结构特别清晰特别适合构建企业级应用。而且它原生支持TypeScript类型检查能帮我们避免很多低级错误。这个实战项目我们会用到这些核心技术TypeORM处理数据库操作class-validator做数据校验分页查询处理大量数据展示模糊搜索提升用户体验2. 项目初始化与基础配置2.1 创建NestJS项目首先确保你安装了Node.js建议16.x以上版本然后全局安装Nest CLInpm i -g nestjs/cli nest new crud-demo安装完成后进入项目目录安装必要依赖npm install nestjs/typeorm typeorm mysql2 class-validator class-transformer这里我选择MySQL作为数据库你也可以换成PostgreSQL或其他TypeORM支持的数据库。2.2 配置TypeORM连接打开app.module.ts配置数据库连接import { Module } from nestjs/common; import { TypeOrmModule } from nestjs/typeorm; Module({ imports: [ TypeOrmModule.forRoot({ type: mysql, host: localhost, port: 3306, username: root, password: yourpassword, database: crud_demo, entities: [__dirname /**/*.entity{.ts,.js}], synchronize: true, // 开发环境可以用生产环境要关掉 }), ], }) export class AppModule {}注意synchronize: true会在应用启动时自动同步实体到数据库开发时很方便但生产环境一定要设为false。3. 用户模块开发3.1 创建用户实体先定义用户实体user.entity.tsimport { Entity, PrimaryGeneratedColumn, Column } from typeorm; Entity() export class User { PrimaryGeneratedColumn() id: number; Column({ length: 50 }) name: string; Column({ length: 500, nullable: true }) desc: string; Column({ default: () CURRENT_TIMESTAMP }) createTime: Date; }这里定义了用户的基本字段TypeORM的装饰器让实体定义非常直观。PrimaryGeneratedColumn表示自增主键Column可以设置字段长度、是否可为空等属性。3.2 创建DTO对象DTOData Transfer Object用于数据传输和验证我们先创建创建用户的DTOimport { IsString, IsNotEmpty, Length } from class-validator; export class CreateUserDto { IsString() IsNotEmpty() Length(2, 50) name: string; IsString() Length(0, 500) desc?: string; }这里使用了class-validator的装饰器来做数据校验IsString()确保是字符串IsNotEmpty()不能为空Length()限制长度范围更新用户的DTO也类似export class UpdateUserDto { IsString() Length(2, 50) name?: string; IsString() Length(0, 500) desc?: string; }3.3 实现Service层用户服务的核心逻辑都在这里import { Injectable } from nestjs/common; import { InjectRepository } from nestjs/typeorm; import { Repository, Like } from typeorm; import { User } from ./user.entity; import { CreateUserDto } from ./dto/create-user.dto; import { UpdateUserDto } from ./dto/update-user.dto; Injectable() export class UserService { constructor( InjectRepository(User) private userRepository: RepositoryUser, ) {} async create(createUserDto: CreateUserDto): PromiseUser { const user this.userRepository.create(createUserDto); return this.userRepository.save(user); } async findAll(query: { keyword?: string; page?: number; pageSize?: number; }): Promise{ data: User[]; total: number } { const { keyword , page 1, pageSize 10 } query; const [data, total] await this.userRepository.findAndCount({ where: { name: Like(%${keyword}%), }, order: { id: DESC }, skip: (page - 1) * pageSize, take: pageSize, }); return { data, total }; } async findOne(id: number): PromiseUser { return this.userRepository.findOneBy({ id }); } async update(id: number, updateUserDto: UpdateUserDto): Promisevoid { await this.userRepository.update(id, updateUserDto); } async remove(id: number): Promisevoid { await this.userRepository.delete(id); } }这里有几个关键点使用InjectRepository注入用户仓库findAll方法实现了分页和模糊搜索Like操作符实现模糊查询findAndCount一次性获取数据和总数性能更好3.4 实现Controller层控制器处理HTTP请求import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from nestjs/common; import { UserService } from ./user.service; import { CreateUserDto } from ./dto/create-user.dto; import { UpdateUserDto } from ./dto/update-user.dto; Controller(users) export class UserController { constructor(private readonly userService: UserService) {} Post() create(Body() createUserDto: CreateUserDto) { return this.userService.create(createUserDto); } Get() findAll( Query(keyword) keyword?: string, Query(page) page?: number, Query(pageSize) pageSize?: number, ) { return this.userService.findAll({ keyword, page, pageSize }); } Get(:id) findOne(Param(id) id: string) { return this.userService.findOne(id); } Patch(:id) update(Param(id) id: string, Body() updateUserDto: UpdateUserDto) { return this.userService.update(id, updateUserDto); } Delete(:id) remove(Param(id) id: string) { return this.userService.remove(id); } }注意路由前缀设为/usersQuery获取查询参数id把字符串ID转为数字3.5 模块整合最后把所有这些整合到用户模块import { Module } from nestjs/common; import { TypeOrmModule } from nestjs/typeorm; import { User } from ./user.entity; import { UserService } from ./user.service; import { UserController } from ./user.controller; Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UserController], providers: [UserService], }) export class UserModule {}4. 高级功能实现4.1 全局异常处理为了更好的错误处理我们可以添加全局异常过滤器import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from nestjs/common; import { Request, Response } from express; Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx host.switchToHttp(); const response ctx.getResponseResponse(); const request ctx.getRequestRequest(); const status exception.getStatus(); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: exception.message || Internal server error, }); } }然后在main.ts中使用app.useGlobalFilters(new HttpExceptionFilter());4.2 数据验证管道NestJS内置了验证管道我们只需要在main.ts中启用app.useGlobalPipes(new ValidationPipe());这样所有DTO的校验规则都会自动生效。4.3 接口文档生成使用Swagger生成API文档npm install nestjs/swagger swagger-ui-express然后在main.ts中配置const config new DocumentBuilder() .setTitle(CRUD Demo) .setDescription(The CRUD API description) .setVersion(1.0) .addTag(users) .build(); const document SwaggerModule.createDocument(app, config); SwaggerModule.setup(api, app, document);现在访问/api就能看到漂亮的API文档了。5. 前端对接实战虽然这不是前端教程但简单展示下如何用Vue3对接我们的APIimport axios from axios; const api axios.create({ baseURL: http://localhost:3000, }); // 获取用户列表 export const getUsers (params) api.get(/users, { params }); // 创建用户 export const createUser (data) api.post(/users, data); // 更新用户 export const updateUser (id, data) api.patch(/users/${id}, data); // 删除用户 export const deleteUser (id) api.delete(/users/${id});前端页面可以这样调用const { data, total } await getUsers({ keyword: searchText.value, page: currentPage.value, pageSize: pageSize.value, });6. 性能优化与最佳实践6.1 数据库索引优化给常用查询字段添加索引Column({ length: 50 }) Index() name: string;6.2 缓存常用查询使用NestJS的缓存模块import { CacheModule } from nestjs/cache-manager; Module({ imports: [CacheModule.register()], }) export class UserModule {}然后在Service中使用Cacheable({ ttl: 60 }) async findAll() { // 查询逻辑 }6.3 分页查询优化对于大数据量分页推荐使用游标分页async findAllAfterId(id: number, limit: number) { return this.userRepository.find({ where: { id: MoreThan(id) }, take: limit, order: { id: ASC }, }); }6.4 事务处理多个操作需要原子性时使用事务async transfer(fromId: number, toId: number, amount: number) { await this.userRepository.manager.transaction(async (manager) { const fromUser await manager.findOneBy(User, { id: fromId }); const toUser await manager.findOneBy(User, { id: toId }); // 转账逻辑 manager.save(User, [ { ...fromUser, balance: fromUser.balance - amount }, { ...toUser, balance: toUser.balance amount }, ]); }); }7. 部署与监控7.1 生产环境配置创建ormconfig.prod.jsmodule.exports { type: mysql, host: process.env.DB_HOST, port: process.env.DB_PORT, username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, entities: [dist/**/*.entity{.ts,.js}], synchronize: false, logging: false, extra: { connectionLimit: 10, }, };7.2 健康检查添加健康检查端点Get(health) healthCheck() { return { status: ok, timestamp: new Date() }; }7.3 日志记录使用NestJS的Loggerprivate readonly logger new Logger(UserService.name); async findAll() { this.logger.log(Fetching all users); // 查询逻辑 }8. 常见问题解决8.1 跨域问题在main.ts中启用CORSapp.enableCors();8.2 时区问题确保数据库和服务器时区一致Column({ default: () CURRENT_TIMESTAMP, transformer: { to: (value) value, from: (value) moment(value).format(YYYY-MM-DD HH:mm:ss), }, }) createTime: Date;8.3 性能监控使用nestjs/terminus添加健康检查import { TerminusModule } from nestjs/terminus; Module({ imports: [TerminusModule], }) export class HealthModule {}9. 项目结构优化推荐的项目结构src/ ├── modules/ │ ├── user/ │ │ ├── dto/ │ │ ├── entities/ │ │ ├── user.controller.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── common/ │ ├── filters/ │ ├── interceptors/ │ └── pipes/ ├── app.module.ts └── main.ts10. 测试策略10.1 单元测试测试Service方法describe(UserService, () { let service: UserService; let repository: RepositoryUser; beforeEach(async () { const module: TestingModule await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), useClass: Repository, }, ], }).compile(); service module.getUserService(UserService); repository module.getRepositoryUser(getRepositoryToken(User)); }); it(should create a user, async () { const userDto { name: Test, desc: Test desc }; const result await service.create(userDto); expect(result).toBeDefined(); expect(result.name).toEqual(userDto.name); }); });10.2 E2E测试测试API端点describe(UserController (e2e), () { let app: INestApplication; beforeAll(async () { const moduleFixture: TestingModule await Test.createTestingModule({ imports: [AppModule], }).compile(); app moduleFixture.createNestApplication(); await app.init(); }); it(/users (GET), () { return request(app.getHttpServer()) .get(/users) .expect(200) .expect((res) { expect(res.body.data).toBeInstanceOf(Array); }); }); });11. 安全加固11.1 输入消毒防止XSS攻击import { sanitize } from class-sanitizer; Post() create(Body() createUserDto: CreateUserDto) { sanitize(createUserDto); // 消毒输入 return this.userService.create(createUserDto); }11.2 速率限制防止暴力破解import { ThrottlerModule } from nestjs/throttler; Module({ imports: [ ThrottlerModule.forRoot({ ttl: 60, limit: 10, }), ], }) export class AppModule {}11.3 敏感数据过滤使用拦截器过滤密码等敏感字段import { map } from rxjs/operators; Injectable() export class RemoveSensitiveFieldsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observableany { return next.handle().pipe( map((data) { if (Array.isArray(data)) { return data.map((item) this.removeFields(item)); } return this.removeFields(data); }), ); } private removeFields(data: any) { if (!data || typeof data ! object) return data; const { password, ...rest } data; return rest; } }12. 扩展功能思路12.1 文件上传实现用户头像上传Post(avatar) UseInterceptors(FileInterceptor(file)) uploadAvatar(UploadedFile() file: Express.Multer.File) { return this.userService.saveAvatar(file); }12.2 消息队列使用Redis处理耗时操作import { Queue } from bull; import { InjectQueue } from nestjs/bull; Injectable() export class UserService { constructor( InjectQueue(email) private emailQueue: Queue, ) {} async create(userDto: CreateUserDto) { const user await this.userRepository.create(userDto); await this.emailQueue.add(welcome, { email: user.email }); return this.userRepository.save(user); } }12.3 微服务拆分将用户服务拆分为独立微服务// main.ts const app await NestFactory.createMicroserviceMicroserviceOptions( AppModule, { transport: Transport.TCP, options: { port: 3001 }, }, ); await app.listen();13. 性能监控与日志13.1 Prometheus监控添加性能指标import { PrometheusModule } from willsoto/nestjs-prometheus; Module({ imports: [PrometheusModule.register()], }) export class MetricsModule {}13.2 ELK日志收集配置结构化日志import { createLogger } from winston; const logger createLogger({ transports: [new transports.Http({ host: logstash, port: 5044 })], }); app.useLogger(logger);14. 持续集成部署14.1 Docker化创建DockerfileFROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build CMD [node, dist/main]14.2 CI/CD配置GitLab CI示例stages: - test - build - deploy test: stage: test script: - npm install - npm run test build: stage: build script: - docker build -t crud-demo . deploy: stage: deploy script: - docker-compose up -d15. 项目总结与经验分享在实际开发中我发现NestJS的模块化设计特别适合团队协作。不同开发者可以并行开发不同模块通过清晰的接口定义来集成。TypeORM虽然强大但在复杂查询时还是建议直接使用QueryBuilder可以获得更好的性能。分页查询时要注意几个坑不要用skip和take处理大数据量分页性能很差模糊搜索LIKE语句要加索引否则全表扫描很慢事务处理要小心死锁问题DTO验证在实际项目中帮我们拦截了至少30%的无效请求大大减轻了服务端压力。建议对所有输入都定义严格的DTO验证规则。最后这个项目虽然看起来简单但涵盖了后端开发的绝大部分核心概念。掌握了这些你就能应对大多数业务场景的开发需求了。

相关新闻

最新新闻

日新闻

周新闻

月新闻