一、服务端
- 项目概述
1.书籍管理
- 创建新书籍 (POST /books)
- 查询所有书籍 (GET /books)
- 查询单本书籍 (GET /books/:id)
- 更新书籍信息 (PATCH /books/:id)
- 删除书籍 (DELETE /books/:id)
2.数据验证
- 使用class-validator进行请求数据验证
- 书名、作者、分类不能为空
- 状态必须是:连载中、已完结或暂停更新
- 字数必须是数字
3、项目结构
- 模块化设计 :使用NestJS的模块化架构,创建了专门的Books模块
- 控制器层 :处理HTTP请求,路由管理
- 服务层 :实现业务逻辑
- DTO :定义数据传输对象,用于验证和传输数据
- 实体类 :定义书籍的数据结构
4、技术栈
- NestJS :基于Node.js的服务器端框架
- TypeScript :提供类型安全
- class-validator :数据验证
- class-transformer :数据转换
- @nestjs/mapped-types :用于DTO类型转换
- 创建NestJS项目
1. 准备工作
//使用npm初始化一个新的NestJS项目
npm init -y
//安装NestJS CLI
npm install -g @nestjs/cli
2.项目搭建
//命名为novel-api
nest new novel-api --package-manager npm
- 书籍模块
//创建一个书籍模块,包括控制器和服务,用于实现书籍的CRUD操作
nest generate module books
//创建书籍控制器,用于处理HTTP请求
nest generate controller books
//创建书籍服务,用于实现业务逻辑
nest generate service books
//创建书籍的DTO(数据传输对象)和实体类,用于定义书籍的数据结构
mkdir src\books\dto
mkdir src\books\entities
1. 创建书籍实体类,定义书籍的数据结构
//novel-api\src\books\entities\book.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('books')
export class Book {
@ApiProperty({ description: '书籍ID', example: 1 })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: '书籍标题', example: '三体' })
@Column({ length: 100 })
title: string;
@ApiProperty({ description: '作者名称', example: '刘慈欣' })
@Column({ length: 50 })
author: string;
@ApiProperty({ description: '书籍描述', example: '科幻小说' })
@Column({ type: 'text' })
description: string;
@ApiProperty({ description: '书籍分类', example: '科幻' })
@Column({ length: 50 })
category: string;
@ApiProperty({ description: '书籍字数', example: 500000 })
@Column({ type: 'int' })
wordCount: number;
@ApiProperty({ description: '书籍状态', example: '已完结', enum: ['连载中', '已完结', '暂停更新'] })
@Column({ length: 20 })
status: string; // 连载中、已完结等
@ApiProperty({ description: '创建时间' })
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@ApiProperty({ description: '更新时间' })
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
2、创建书籍的DTO类,用于验证和传输创建书籍的数据
//novel-api\src\books\dto\create-book.dto.ts
import { IsNotEmpty, IsString, IsNumber, IsOptional, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export enum BookStatus {
ONGOING = '连载中',
COMPLETED = '已完结',
PAUSED = '暂停更新'
}
export class CreateBookDto {
@ApiProperty({ description: '书籍标题', example: '三体', required: true })
@IsNotEmpty({ message: '书名不能为空' })
@IsString({ message: '书名必须是字符串' })
title: string;
@ApiProperty({ description: '作者名称', example: '刘慈欣', required: true })
@IsNotEmpty({ message: '作者不能为空' })
@IsString({ message: '作者必须是字符串' })
author: string;
@ApiProperty({ description: '书籍描述', example: '科幻小说', required: false })
@IsString({ message: '描述必须是字符串' })
@IsOptional()
description: string;
@ApiProperty({ description: '书籍分类', example: '科幻', required: true })
@IsString({ message: '分类必须是字符串' })
@IsNotEmpty({ message: '分类不能为空' })
category: string;
@ApiProperty({ description: '书籍字数', example: 500000, required: false })
@IsNumber({}, { message: '字数必须是数字' })
@IsOptional()
wordCount: number;
@ApiProperty({ description: '书籍状态', example: '已完结', enum: BookStatus, enumName: 'BookStatus', required: true })
@IsEnum(BookStatus, { message: '状态必须是:连载中、已完结或暂停更新' })
@IsNotEmpty({ message: '状态不能为空' })
status: string;
}
3、创建更新书籍的DTO类,用于验证和传输更新书籍的数据
//novel-api\src\books\dto\update-book.dto.ts
import { PartialType } from '@nestjs/swagger';
import { CreateBookDto } from './create-book.dto';
// 使用PartialType将CreateBookDto中的所有属性变为可选
export class UpdateBookDto extends PartialType(CreateBookDto) {}
4、书籍服务,提供书籍的CRUD操作
//novel-api\src\books\books.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
import { Book } from './entities/book.entity';
@Injectable()
export class BooksService {
constructor(
@InjectRepository(Book)
private bookRepository: Repository<Book>,
) {}
async create(createBookDto: CreateBookDto): Promise<Book> {
const book = this.bookRepository.create(createBookDto);
return await this.bookRepository.save(book);
}
async findAll(): Promise<Book[]> {
return await this.bookRepository.find();
}
async findOne(id: number): Promise<Book> {
const book = await this.bookRepository.findOne({ where: { id } });
if (!book) {
throw new NotFoundException(`Book with ID ${id} not found`);
}
return book;
}
async update(id: number, updateBookDto: UpdateBookDto): Promise<Book> {
const book = await this.findOne(id);
// 合并更新的数据
const updatedBook = this.bookRepository.merge(book, updateBookDto);
// 保存更新后的数据
return await this.bookRepository.save(updatedBook);
}
async remove(id: number): Promise<void> {
const book = await this.findOne(id);
await this.bookRepository.remove(book);
}
}
//novel-api\src\books\books.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';
import { Book } from './entities/book.entity';
@Module({
imports: [TypeOrmModule.forFeature([Book])],
controllers: [BooksController],
providers: [BooksService]
})
export class BooksModule {}
5、书籍控制器的CRUD方法,处理HTTP请求并调用服务层的方法
//novel-api\src\books\books.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, HttpStatus, HttpCode } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
@ApiTags('books')
@Controller('books')
export class BooksController {
constructor(private readonly booksService: BooksService) {}
@ApiOperation({ summary: '创建书籍', description: '创建一本新书籍' })
@ApiBody({ type: CreateBookDto })
@ApiResponse({ status: HttpStatus.CREATED, description: '书籍创建成功', type: CreateBookDto })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '请求参数验证失败' })
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createBookDto: CreateBookDto) {
return await this.booksService.create(createBookDto);
}
@ApiOperation({ summary: '获取所有书籍', description: '获取所有书籍的列表' })
@ApiResponse({ status: HttpStatus.OK, description: '成功获取所有书籍', type: [CreateBookDto] })
@Get()
async findAll() {
return await this.booksService.findAll();
}
@ApiOperation({ summary: '获取单本书籍', description: '根据ID获取单本书籍的详细信息' })
@ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
@ApiResponse({ status: HttpStatus.OK, description: '成功获取书籍', type: CreateBookDto })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return await this.booksService.findOne(id);
}
@ApiOperation({ summary: '更新书籍', description: '更新指定ID的书籍信息' })
@ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
@ApiBody({ type: UpdateBookDto })
@ApiResponse({ status: HttpStatus.OK, description: '书籍更新成功', type: CreateBookDto })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '请求参数验证失败' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
@Patch(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateBookDto: UpdateBookDto,
) {
return await this.booksService.update(id, updateBookDto);
}
@ApiOperation({ summary: '删除书籍', description: '删除指定ID的书籍' })
@ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
@ApiResponse({ status: HttpStatus.OK, description: '书籍删除成功' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number) {
await this.booksService.remove(id);
return null;
}
}
6、安装class-validator和class-transformer,用于验证请求数据
npm install class-validator class-transformer
7、main.ts文件,启用全局验证管道
//novel-api\src\main.ts
import * as dotenv from 'dotenv';
// 加载环境变量(在导入其他模块之前)
dotenv.config();
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用全局验证管道
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 过滤掉未在DTO中声明的属性
transform: true, // 自动转换类型
forbidNonWhitelisted: true, // 禁止未在DTO中声明的属性
transformOptions: {
enableImplicitConversion: true, // 启用隐式类型转换
},
}));
// 启用CORS
app.enableCors();
// 配置Swagger文档
const config = new DocumentBuilder()
.setTitle('网文系统API')
.setDescription('网文系统的API文档,包括书籍的CRUD操作')
.setVersion('1.0')
.addTag('books', '书籍相关接口')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`应用已启动,监听端口:${port}`);
console.log(`API文档地址: http://localhost:${port}/api-docs`);
}
bootstrap();
8、启动应用程序
安装@nestjs/mapped-types包,以便使用PartialType。
安装Swagger相关的包,以便生成API文档
npm install @nestjs/swagger swagger-ui-express
npm install @nestjs/mapped-types
//启动
npm run start:dev
3.数据库接入
- 安装TypeORM和MySQL相关的依赖包
npm install @nestjs/typeorm typeorm mysql2
- 数据库配置文件
//novel-api\src\config\database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const databaseConfig: TypeOrmModuleOptions = {
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_DATABASE || 'novel_db',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production', // 自动同步数据库结构,生产环境中应该禁用
logging: process.env.NODE_ENV !== 'production',
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
migrationsRun: true, // 应用启动时自动运行迁移
};
//novel-api\src\app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BooksModule } from './books/books.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { databaseConfig } from './config/database.config';
import { DatabaseModule } from './database/database.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [
TypeOrmModule.forRoot(databaseConfig),
DatabaseModule,
BooksModule,
HealthModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
//novel-api\.env
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=666666
DB_DATABASE=novel_db
# 应用配置
PORT=3000
NODE_ENV=development
- 安装dotenv模块来加载环境变量
npm install dotenv
- 初始化数据库
-- 创建数据库
CREATE DATABASE IF NOT EXISTS novel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE novel_db;
-- 创建书籍表(仅供参考,实际表结构会由TypeORM自动创建)
CREATE TABLE IF NOT EXISTS books (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
author VARCHAR(50) NOT NULL,
description TEXT,
category VARCHAR(50) NOT NULL,
word_count INT,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
- 数据库服务
//创建一个数据库服务,用于提供数据库连接和操作
//novel-api\src\database\database.service.ts
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class DatabaseService {
constructor(
@InjectDataSource()
private dataSource: DataSource,
) {}
/**
* 获取数据库连接
*/
getDataSource(): DataSource {
return this.dataSource;
}
/**
* 执行原始SQL查询
* @param query SQL查询语句
* @param parameters 查询参数
*/
async executeQuery(query: string, parameters: any[] = []): Promise<any> {
return await this.dataSource.query(query, parameters);
}
/**
* 检查数据库连接状态
*/
async checkConnection(): Promise<boolean> {
try {
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize();
}
return this.dataSource.isInitialized;
} catch (error) {
console.error('数据库连接失败:', error);
return false;
}
}
}
//创建一个数据库模块,用于提供数据库服务
//novel-api\src\database\database.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}