三、后端项目初始化,NestJS 基础搭建与模块化设计

11 阅读6分钟

前言

在全栈项目开发中,后端项目的初始化和架构设计是构建稳定、可扩展应用的关键。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               # 项目依赖

(二)模块划分原则

模块划分应遵循以下原则:

  1. 按业务功能划分:将相关的控制器、服务、模型等组件划分为一个模块,如用户管理模块、订单管理模块等。
  2. 解耦合:模块之间应尽量解耦,减少相互依赖,提高模块的独立性和可复用性。
  3. 高内聚:模块内部的组件应紧密相关,共同完成特定的功能。
  4. 可扩展:模块设计应考虑未来的扩展性,便于添加新的功能或修改现有功能。

(三)模块之间的依赖

模块之间可以通过依赖注入的方式相互引用。在模块的 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 的后端开发。