前言
在全栈项目开发中,后端项目的初始化和架构设计是构建稳定、可扩展应用的关键。NestJS 作为一个现代化的 Node.js 框架,凭借其模块化设计和强大的功能,为后端开发提供了坚实的基础。本章将深入探讨如何从零开始初始化一个 NestJS 项目,并实现模块化设计,为后续开发打下坚实基础。
一、创建 NestJS 项目
(一)安装 NestJS CLI
NestJS CLI 是一个命令行工具,可以帮助我们快速创建和管理 NestJS 项目。首先,全局安装 NestJS CLI:
npm install -g @nestjs/cli
(二)创建项目
使用 NestJS CLI 创建一个新的 NestJS 项目:
nestjs new backend
这将创建一个名为 backend
的文件夹,并初始化一个基本的 NestJS 项目结构。
(三)进入项目目录
cd backend
(四)安装依赖
npm install
(五)启动开发服务器
npm run start:dev
现在,你的 NestJS 项目已经启动,可以通过 http://localhost:3000
访问。
二、NestJS 核心概念
(一)模块
NestJS 使用模块来组织应用程序的结构。模块是一个包含相关功能的组件集合,可以是一个特定的业务功能(如用户管理、订单管理)或其他相关功能的集合。模块通过 @Module
装饰器定义。
例如,创建一个 users
模块:
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
@Module({
controllers: [UsersController],
providers: [UsersService]
})
export class UsersModule {}
(二)控制器
控制器负责处理 HTTP 请求,并返回响应。控制器通过 @Controller
装饰器定义。控制器中的每个方法对应一个路由,处理特定类型的 HTTP 请求。
例如,创建一个 UsersController
:
import { Controller, Get } from '@nestjs/common'
import { UsersService } from './users.service'
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll()
}
}
(三)服务
服务包含业务逻辑,通常不直接处理 HTTP 请求。服务通过 @Injectable
装饰器定义,并通过依赖注入的方式被控制器或其他服务使用。
例如,创建一个 UsersService
:
import { Injectable } from '@nestjs/common'
@Injectable()
export class UsersService {
private users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' }
]
findAll() {
return this.users
}
}
三、模块化设计
(一)项目结构
合理的项目结构是模块化设计的基础。以下是一个典型的 NestJS 项目结构:
backend/
├── src/
│ ├── app.module.ts # 根模块
│ ├── main.ts # 应用入口文件
│ ├── modules/ # 业务模块
│ │ ├── users/ # 用户模块
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ ├── users.module.ts
│ │ │ ├── user.entity.ts
│ │ │ └── users.providers.ts
│ │ ├── auth/ # 认证模块
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── auth.module.ts
│ │ │ └── auth.guard.ts
│ │ └── ... # 其他业务模块
│ ├── common/ # 公共模块
│ │ ├── dto/ # 数据传输对象
│ │ ├── guards/ # 认证守卫
│ │ ├── filters/ # 异常过滤器
│ │ └── interceptors/ # 拦截器
│ ├── config/ # 配置文件
│ │ ├── database.config.ts # 数据库配置
│ │ └── jwt.config.ts # JWT 配置
│ └── ... # 其他公共模块
├── test/ # 测试文件
└── package.json # 项目依赖
(二)模块划分原则
模块划分应遵循以下原则:
- 按业务功能划分:将相关的控制器、服务、模型等组件划分为一个模块,如用户管理模块、订单管理模块等。
- 解耦合:模块之间应尽量解耦,减少相互依赖,提高模块的独立性和可复用性。
- 高内聚:模块内部的组件应紧密相关,共同完成特定的功能。
- 可扩展:模块设计应考虑未来的扩展性,便于添加新的功能或修改现有功能。
(三)模块之间的依赖
模块之间可以通过依赖注入的方式相互引用。在模块的 providers
数组中定义服务,在 imports
数组中导入其他模块,从而实现模块之间的依赖关系。
例如,AuthModule
依赖 UsersModule
:
import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { UsersModule } from '../users/users.module'
@Module({
imports: [UsersModule],
controllers: [AuthController],
providers: [AuthService]
})
export class AuthModule {}
(四)共享服务
如果多个模块需要使用相同的服务,可以将该服务定义为共享服务。在根模块中提供该服务,并在需要的模块中注入。
例如,创建一个共享服务 LoggerService
:
import { Injectable } from '@nestjs/common'
@Injectable()
export class LoggerService {
log(message: string) {
console.log(`[Logger] ${message}`)
}
}
在根模块中提供该服务:
import { Module } from '@nestjs/common'
import { LoggerService } from './common/logger.service'
@Module({
providers: [LoggerService],
exports: [LoggerService]
})
export class AppModule {}
在其他模块中注入该服务:
import { Module } from '@nestjs/common'
import { LoggerService } from '../common/logger.service'
@Module({
providers: [LoggerService]
})
export class UsersModule {}
四、配置环境变量
在 NestJS 项目中,配置环境变量可以帮助我们分离开发环境和生产环境的配置,提高应用的安全性和灵活性。
(一)安装 config
模块
npm install @nestjs/config
(二)创建环境变量文件
在项目根目录下创建 .env
文件:
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=root
DATABASE_NAME=nestjs_db
JWT_SECRET=secret_key
(三)配置 config
模块
在 config/
目录下创建 database.config.ts
文件:
import { ConfigService } from '@nestjs/config'
export class DatabaseConfig {
constructor(private configService: ConfigService) {}
get host() {
return this.configService.get('DATABASE_HOST')
}
get port() {
return this.configService.get('DATABASE_PORT')
}
get user() {
return this.configService.get('DATABASE_USER')
}
get password() {
return this.configService.get('DATABASE_PASSWORD')
}
get name() {
return this.configService.get('DATABASE_NAME')
}
}
在 config/
目录下创建 jwt.config.ts
文件:
import { ConfigService } from '@nestjs/config'
export class JwtConfig {
constructor(private configService: ConfigService) {}
get secret() {
return this.configService.get('JWT_SECRET')
}
}
(四)在模块中使用配置
在 app.module.ts
中引入配置模块:
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { DatabaseConfig } from './config/database.config'
import { JwtConfig } from './config/jwt.config'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true
})
],
providers: [DatabaseConfig, JwtConfig]
})
export class AppModule {}
现在,可以在服务中使用配置:
import { Injectable } from '@nestjs/common'
import { InjectConfig } from '@nestjs/config'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class UsersService {
constructor(private configService: ConfigService) {}
findAll() {
const dbHost = this.configService.get('DATABASE_HOST')
console.log(`Database Host: ${dbHost}`)
// 其他业务逻辑
}
}
五、使用 MySQL 进行数据库操作
(一)安装 MySQL
在开始使用 MySQL 之前,需要先安装 MySQL 数据库。可以通过以下命令安装 MySQL:
# 在 Ubuntu 上安装 MySQL
sudo apt update
sudo apt install mysql-server
# 配置 MySQL
sudo mysql_secure_installation
# 启动 MySQL 服务
sudo systemctl start mysql.service
# 设置 MySQL 开机自启
sudo systemctl enable mysql.service
(二)创建数据库
连接到 MySQL 数据库并创建一个新的数据库:
mysql -u root -p
CREATE DATABASE nestjs_db;
(三)安装 Prisma
Prisma 是一个现代化的 ORM(对象关系映射)工具,可以帮助我们更方便地进行数据库操作。在 NestJS 项目中集成 Prisma,可以提高开发效率并确保数据操作的安全性。
npm install prisma @prisma/client
(四)初始化 Prisma
npx prisma init
这将创建一个 prisma/
目录,包含 schema.prisma
文件和 .env
文件。
(五)配置 MySQL 数据库
在 prisma/schema.prisma
文件中定义数据模型:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
}
在 .env
文件中配置数据库连接:
DATABASE_URL="mysql://root:root@localhost:3306/nestjs_db"
(六)生成 Prisma 客户端
npx prisma generate
(七)创建数据库迁移
npx prisma migrate dev --name init
这将根据 schema.prisma
文件生成数据库迁移脚本,并应用到数据库中。
(八)在 NestJS 中使用 Prisma
创建一个 Prisma 服务,用于管理数据库连接:
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect()
}
async onModuleDestroy() {
await this.$.disconnect()
}
}
在 app.module.ts
中提供 Prisma 服务:
import { Module } from '@nestjs/common'
import { PrismaService } from './prisma.service'
@Module({
providers: [PrismaService],
exports: [PrismaService]
})
export class AppModule {}
现在,可以在服务中使用 Prisma 进行数据库操作:
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma.service'
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll() {
return await this.prisma.user.findMany()
}
async findOne(id: number) {
return await this.prisma.user.findUnique({
where: { id }
})
}
async create(name: string, email: string) {
return await this.prisma.user.create({
data: { name, email }
})
}
async update(id: number, name: string, email: string) {
return await this.prisma.user.update({
where: { id },
data: { name, email }
})
}
async delete(id: number) {
return await this.prisma.user.delete({
where: { id }
})
}
}
六、编写自动化测试
(一)单元测试
在 NestJS 中,可以使用 Jest 进行单元测试。Jest 是一个流行的 JavaScript 测试框架,提供了丰富的功能和易用的 API。
安装 Jest:
npm install jest @types/jest ts-jest
配置 Jest,在 jest.config.js
文件中:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testMatch: ['**/__tests__/**/*.ts'],
transform: {
'^.+\\.(t|j)s$': 'ts-jest'
}
}
创建一个测试文件 users.service.spec.ts
:
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { PrismaService } from '../prisma.service'
describe('UsersService', () => {
let service: UsersService
let prisma: PrismaService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService, PrismaService]
}).compile()
service = module.get<UsersService>(UsersService)
prisma = module.get<PrismaService>(PrismaService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
it('should create a user', async () => {
const user = await service.create('John Doe', 'john@example.com')
expect(user.name).toBe('John Doe')
expect(user.email).toBe('john@example.com')
})
it('should find all users', async () => {
await service.create('John Doe', 'john@example.com')
await service.create('Jane Doe', 'jane@example.com')
const users = await service.findAll()
expect(users.length).toBe(2)
})
it('should find a user by id', async () => {
const user = await service.create('John Doe', 'john@example.com')
const foundUser = await service.findOne(user.id)
expect(foundUser).toEqual(user)
})
it('should update a user', async () => {
const user = await service.create('John Doe', 'john@example.com')
const updatedUser = await service.update(user.id, 'Jane Doe', 'jane@example.com')
expect(updatedUser.name).toBe('Jane Doe')
expect(updatedUser.email).toBe('jane@example.com')
})
it('should delete a user', async () => {
const user = await service.create('John Doe', 'john@example.com')
await service.delete(user.id)
const foundUser = await service.findOne(user.id)
expect(foundUser).toBeNull()
})
})
运行测试:
npm test
(二)集成测试
集成测试用于测试模块之间的交互和应用的整体功能。创建一个测试文件 app.e2e-spec.ts
:
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import { AppModule } from '../src/app.module'
import * as request from 'supertest'
describe('AppController (e2e)', () => {
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule]
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
afterAll(async () => {
await app.close()
})
it('/users (GET)', async () => {
const response = await request(app.getHttpServer())
.get('/users')
.expect(200)
expect(response.body.length).toBeGreaterThanOrEqual(0)
})
it('/users (POST)', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({ name: 'John Doe', email: 'john@example.com' })
.expect(201)
expect(response.body.name).toBe('John Doe')
expect(response.body.email).toBe('john@example.com')
})
it('/users/:id (GET)', async () => {
const createResponse = await request(app.getHttpServer())
.post('/users')
.send({ name: 'John Doe', email: 'john@example.com' })
const response = await request(app.getHttpServer())
.get(`/users/${createResponse.body.id}`)
.expect(200)
expect(response.body).toEqual(createResponse.body)
})
it('/users/:id (PUT)', async () => {
const createResponse = await request(app.getHttpServer())
.post('/users')
.send({ name: 'John Doe', email: 'john@example.com' })
const response = await request(app.getHttpServer())
.put(`/users/${createResponse.body.id}`)
.send({ name: 'Jane Doe', email: 'jane@example.com' })
.expect(200)
expect(response.body.name).toBe('Jane Doe')
expect(response.body.email).toBe('jane@example.com')
})
it('/users/:id (DELETE)', async () => {
const createResponse = await request(app.getHttpServer())
.post('/users')
.send({ name: 'John Doe', email: 'john@example.com' })
await request(app.getHttpServer())
.delete(`/users/${createResponse.body.id}`)
.expect(200)
const response = await request(app.getHttpServer())
.get(`/users/${createResponse.body.id}`)
.expect(404)
})
})
运行集成测试:
npm run test:e2e
七、总结
通过本章的学习,我们完成了 NestJS 项目的初始化工作。从创建项目、配置别名和环境变量,到集成 Prisma 和 MySQL 等现代工具链,每一个步骤都为项目的开发奠定了坚实基础。
我们还学习了如何使用 Prisma 进行 MySQL 数据库操作,如何编写自动化测试,以及如何进行模块化设计。
在接下来的章节中,我们将继续深入学习前后端协作和接口联调等内容,逐步构建完整的全栈应用。
希望本章内容能为你提供清晰的指导,帮助你快速上手 NestJS + MySQL 的后端开发。