NestJS + TypeScript 入门完整教程

目录

  1. 什么是 NestJS
  2. 环境准备
  3. 项目初始化
  4. 核心概念
  5. 实战项目:博客 API
  6. 数据库集成 (PostgreSQL)
  7. Docker 部署

什么是 NestJS

NestJS 是一个渐进式 Node.js 框架,用于构建高效、可扩展的服务端应用。它提供了开箱即用的应用程序架构,允许开发者和团队创建高度可测试、可维护和可扩展的应用程序。

NestJS vs Express vs Fastify

特性ExpressFastifyNestJS
框架结构轻量级、灵活轻量级、高性能完整框架,开箱即用
装饰器支持
TypeScript✅ (需手动配置)✅ (需手动配置)✅ (原生支持)
依赖注入
模块化设计
学习曲线平缓平缓陡峭
性能中等

选择 NestJS 的理由

  • 企业级架构,适合大型项目
  • 完整的生态系统(ORM、测试、CLI 工具)
  • TypeScript 原生支持,强类型检查
  • 依赖注入容器,易于测试和维护
  • 活跃社区和完善文档

环境准备

系统要求

  • Node.js 18.0+
  • npm 9.0+ 或 yarn 4.0+
  • PostgreSQL 12+ (可选,后续用到)
  • Docker (可选,部署时用到)

检查环境

# 检查 Node.js 版本
node --version

# 检查 npm 版本
npm --version

# 安装 NestJS CLI (全局)
npm install -g @nestjs/cli

# 验证 NestJS CLI 安装
nest --version

项目初始化

方式一:使用 NestJS CLI (推荐)

# 创建新项目
nest new blog-api

# 进入项目目录
cd blog-api

# 选择包管理器 (npm 或 yarn),这里选择 npm

# 安装依赖
npm install

# 启动开发服务器
npm run start:dev

方式二:使用 Git 克隆官方模板

git clone https://github.com/nestjs/typescript-starter.git blog-api
cd blog-api
npm install
npm run start:dev

项目目录结构

blog-api/
├── src/
│   ├── app.controller.ts       # 主控制器
│   ├── app.controller.spec.ts  # 控制器单元测试
│   ├── app.module.ts          # 根模块
│   ├── app.service.ts         # 主服务
│   └── main.ts                # 应用入口
├── test/
│   └── app.e2e-spec.ts        # 端到端测试
├── dist/                       # 编译后的代码 (生成)
├── node_modules/              # 依赖包
├── .env                        # 环境变量 (需创建)
├── nest-cli.json             # NestJS CLI 配置
├── tsconfig.json             # TypeScript 配置
└── package.json              # 项目元数据

核心概念

1. 模块 (Module)

模块是用 @Module() 装饰器标注的类。用于组织相关功能。

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  // 导入其他模块
  imports: [],
  // 声明当前模块的控制器
  controllers: [UsersController],
  // 声明当前模块的提供者 (服务)
  providers: [UsersService],
  // 导出提供者给其他模块使用
  exports: [UsersService],
})
export class UsersModule {}

2. 控制器 (Controller)

负责处理 HTTP 请求和返回响应。

// src/users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Param,
  Body,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  // 依赖注入:自动实例化 UsersService
  constructor(private readonly usersService: UsersService) {}

  // GET /users - 获取所有用户
  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  // GET /users/:id - 获取单个用户
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  // POST /users - 创建用户
  @Post()
  @HttpCode(HttpStatus.CREATED) // 响应状态码 201
  create(@Body() createUserDto: any) {
    return this.usersService.create(createUserDto);
  }

  // PUT /users/:id - 更新用户
  @Put(':id')
  update(
    @Param('id') id: string,
    @Body() updateUserDto: any
  ) {
    return this.usersService.update(+id, updateUserDto);
  }

  // DELETE /users/:id - 删除用户
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT) // 响应状态码 204
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

3. 服务 (Service)

包含业务逻辑,通常由控制器调用。

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  // 模拟数据库
  private users: any[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  // 获取所有用户
  findAll() {
    return this.users;
  }

  // 获取单个用户
  findOne(id: number) {
    return this.users.find(user => user.id === id);
  }

  // 创建用户
  create(createUserDto: any) {
    const newUser = {
      id: Math.max(...this.users.map(u => u.id)) + 1,
      ...createUserDto,
    };
    this.users.push(newUser);
    return newUser;
  }

  // 更新用户
  update(id: number, updateUserDto: any) {
    const user = this.findOne(id);
    if (!user) return null;
    Object.assign(user, updateUserDto);
    return user;
  }

  // 删除用户
  remove(id: number) {
    const index = this.users.findIndex(user => user.id === id);
    if (index > -1) {
      return this.users.splice(index, 1);
    }
    return null;
  }
}

4. 装饰器 (Decorators)

装饰器是 TypeScript 功能,NestJS 大量使用它来标记类和方法。

// 常用装饰器对照表
@Module()           // 声明模块
@Controller()       // 声明控制器
@Injectable()       // 声明提供者 (通常是服务)
@Get()              // GET 请求
@Post()             // POST 请求
@Put()              // PUT 请求
@Delete()           // DELETE 请求
@Param()            // 路由参数
@Body()             // 请求体
@Query()            // 查询参数
@Headers()          // 请求头
@HttpCode()         // HTTP 状态码
@Inject()           // 注入依赖
@Middleware()       // 中间件
@Guard()            // 守卫 (如认证)
@Pipe()             // 管道 (验证、转换)
@UseInterceptors()  // 拦截器
@UseGuards()        // 使用守卫
@UsePipes()         // 使用管道

5. DTO (Data Transfer Object)

定义数据的形状,用于 API 请求和响应验证。

// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// 继承 CreateUserDto,所有字段都变为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {}

实战项目:博客 API

现在让我们构建一个简单的博客 API,包含用户、文章、评论功能。

第一步:生成模块和资源

使用 NestJS CLI 快速生成模块、控制器、服务、DTO:

# 生成文章模块 (包含控制器、服务、DTO)
nest g module posts
nest g controller posts
nest g service posts

# 生成评论模块
nest g module comments
nest g controller comments
nest g service comments

# 生成用户模块
nest g module users
nest g controller users
nest g service users

第二步:创建 DTO

// src/posts/dto/create-post.dto.ts
import { IsString, IsOptional, MinLength, MaxLength } from 'class-validator';

export class CreatePostDto {
  @IsString()
  @MinLength(5)
  title: string;

  @IsString()
  @MinLength(10)
  content: string;

  @IsString()
  @IsOptional()
  @MaxLength(200)
  summary?: string;

  @IsString()
  @IsOptional()
  author?: string;
}

// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';

export class UpdatePostDto extends PartialType(CreatePostDto) {}
// src/comments/dto/create-comment.dto.ts
import { IsString, IsNumber, MinLength } from 'class-validator';

export class CreateCommentDto {
  @IsNumber()
  postId: number;

  @IsString()
  author: string;

  @IsString()
  @MinLength(3)
  content: string;
}

第三步:实现服务

// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

export interface Post {
  id: number;
  title: string;
  content: string;
  summary?: string;
  author: string;
  createdAt: Date;
  updatedAt: Date;
}

@Injectable()
export class PostsService {
  // 模拟数据库存储
  private posts: Post[] = [
    {
      id: 1,
      title: 'NestJS 入门指南',
      content: '这是一篇关于 NestJS 框架的介绍文章...',
      summary: 'NestJS 是一个渐进式 Node.js 框架',
      author: 'Admin',
      createdAt: new Date('2024-01-01'),
      updatedAt: new Date('2024-01-01'),
    },
  ];

  // 获取所有文章
  findAll(): Post[] {
    return this.posts;
  }

  // 根据 ID 获取单个文章
  findOne(id: number): Post {
    const post = this.posts.find(p => p.id === id);
    if (!post) {
      throw new NotFoundException(`文章 ID ${id} 不存在`);
    }
    return post;
  }

  // 创建文章
  create(createPostDto: CreatePostDto): Post {
    const newPost: Post = {
      id: Math.max(...this.posts.map(p => p.id), 0) + 1,
      ...createPostDto,
      author: createPostDto.author || 'Anonymous',
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    this.posts.push(newPost);
    return newPost;
  }

  // 更新文章
  update(id: number, updatePostDto: UpdatePostDto): Post {
    const post = this.findOne(id);
    Object.assign(post, updatePostDto, { updatedAt: new Date() });
    return post;
  }

  // 删除文章
  remove(id: number): Post {
    const post = this.findOne(id);
    const index = this.posts.indexOf(post);
    this.posts.splice(index, 1);
    return post;
  }
}
// src/comments/comments.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { CreateCommentDto } from './dto/create-comment.dto';
import { PostsService } from '../posts/posts.service';

export interface Comment {
  id: number;
  postId: number;
  author: string;
  content: string;
  createdAt: Date;
}

@Injectable()
export class CommentsService {
  private comments: Comment[] = [];
  private commentId = 1;

  // 注入 PostsService 用于验证文章是否存在
  constructor(private postsService: PostsService) {}

  // 获取特定文章的所有评论
  findByPostId(postId: number): Comment[] {
    this.postsService.findOne(postId); // 验证文章存在
    return this.comments.filter(c => c.postId === postId);
  }

  // 创建评论
  create(createCommentDto: CreateCommentDto): Comment {
    // 验证文章是否存在
    this.postsService.findOne(createCommentDto.postId);

    const newComment: Comment = {
      id: this.commentId++,
      ...createCommentDto,
      createdAt: new Date(),
    };
    this.comments.push(newComment);
    return newComment;
  }

  // 删除评论
  remove(id: number): Comment {
    const index = this.comments.findIndex(c => c.id === id);
    if (index === -1) {
      throw new BadRequestException(`评论 ID ${id} 不存在`);
    }
    return this.comments.splice(index, 1)[0];
  }
}

第四步:实现控制器

// src/posts/posts.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Put,
  Delete,
  HttpCode,
  HttpStatus,
  ValidationPipe,
  ParseIntPipe,
} from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  // GET /posts - 获取所有文章
  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  // GET /posts/:id - 获取单个文章
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    // ParseIntPipe 自动将 string 转为 number
    return this.postsService.findOne(id);
  }

  // POST /posts - 创建文章
  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body(ValidationPipe) createPostDto: CreatePostDto) {
    // ValidationPipe 自动验证 DTO
    return this.postsService.create(createPostDto);
  }

  // PUT /posts/:id - 更新文章
  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body(ValidationPipe) updatePostDto: UpdatePostDto,
  ) {
    return this.postsService.update(id, updatePostDto);
  }

  // DELETE /posts/:id - 删除文章
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.remove(id);
  }
}
// src/comments/comments.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Delete,
  HttpCode,
  HttpStatus,
  ValidationPipe,
  ParseIntPipe,
} from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CreateCommentDto } from './dto/create-comment.dto';

@Controller('comments')
export class CommentsController {
  constructor(private readonly commentsService: CommentsService) {}

  // GET /comments/post/:postId - 获取特定文章的评论
  @Get('post/:postId')
  findByPostId(@Param('postId', ParseIntPipe) postId: number) {
    return this.commentsService.findByPostId(postId);
  }

  // POST /comments - 创建评论
  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body(ValidationPipe) createCommentDto: CreateCommentDto) {
    return this.commentsService.create(createCommentDto);
  }

  // DELETE /comments/:id - 删除评论
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.commentsService.remove(id);
  }
}

第五步:配置根模块

// src/app.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { CommentsModule } from './comments/comments.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [PostsModule, CommentsModule, UsersModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

确保 comments.module.ts 导入 PostsModule:

// src/comments/comments.module.ts
import { Module } from '@nestjs/common';
import { CommentsController } from './comments.controller';
import { CommentsService } from './comments.service';
import { PostsModule } from '../posts/posts.module';

@Module({
  imports: [PostsModule], // 导入 PostsModule 使用 PostsService
  controllers: [CommentsController],
  providers: [CommentsService],
})
export class CommentsModule {}

第六步:测试 API

启动开发服务器:

npm run start:dev

使用 curl 或 Postman 测试:

# 获取所有文章
curl http://localhost:3000/posts

# 创建文章
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "TypeScript 最佳实践",
    "content": "这是一篇关于 TypeScript 的文章...",
    "author": "Dev Team"
  }'

# 获取评论
curl http://localhost:3000/comments/post/1

# 创建评论
curl -X POST http://localhost:3000/comments \
  -H "Content-Type: application/json" \
  -d '{
    "postId": 1,
    "author": "User123",
    "content": "很有用的文章!"
  }'

# 删除评论
curl -X DELETE http://localhost:3000/comments/1

数据库集成 (PostgreSQL)

现在将内存数据源替换为真实数据库。我们使用 TypeORM 和 PostgreSQL。

安装依赖

npm install @nestjs/typeorm typeorm pg

配置数据库连接

创建 .env 文件:

# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=blog_db
DATABASE_USER=postgres
DATABASE_PASSWORD=password123
DATABASE_SYNCHRONIZE=true

安装 dotenv 包用于读取环境变量:

npm install dotenv

定义实体

// src/posts/entities/post.entity.ts
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { Comment } from '../../comments/entities/comment.entity';

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 255 })
  title: string;

  @Column({ type: 'text' })
  content: string;

  @Column({ type: 'varchar', length: 200, nullable: true })
  summary: string;

  @Column({ type: 'varchar', length: 100, default: 'Anonymous' })
  author: string;

  // 一对多关系:一篇文章有多条评论
  @OneToMany(() => Comment, comment => comment.post, { cascade: true })
  comments: Comment[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}
// src/comments/entities/comment.entity.ts
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  ManyToOne,
  JoinColumn,
} from 'typeorm';
import { Post } from '../../posts/entities/post.entity';

@Entity('comments')
export class Comment {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 100 })
  author: string;

  @Column({ type: 'text' })
  content: string;

  @CreateDateColumn()
  createdAt: Date;

  // 多对一关系:多条评论属于一篇文章
  @ManyToOne(() => Post, post => post.comments, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'postId' })
  post: Post;

  @Column()
  postId: number;
}

更新 App Module

// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsModule } from './posts/posts.module';
import { CommentsModule } from './comments/comments.module';
import { Post } from './posts/entities/post.entity';
import { Comment } from './comments/entities/comment.entity';

@Module({
  imports: [
    // 配置 TypeORM
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DATABASE_HOST || 'localhost',
      port: parseInt(process.env.DATABASE_PORT || '5432'),
      username: process.env.DATABASE_USER || 'postgres',
      password: process.env.DATABASE_PASSWORD || 'password',
      database: process.env.DATABASE_NAME || 'blog_db',
      entities: [Post, Comment],
      synchronize: process.env.NODE_ENV !== 'production', // 自动同步数据库schema
      logging: false,
    }),
    PostsModule,
    CommentsModule,
  ],
})
export class AppModule {}

更新模块以使用 TypeORM

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { Post } from './entities/post.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}

更新服务使用数据库

// src/posts/posts.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>,
  ) {}

  // 获取所有文章
  async findAll(): Promise<Post[]> {
    return await this.postsRepository.find({
      relations: ['comments'], // 关联加载评论
    });
  }

  // 获取单个文章
  async findOne(id: number): Promise<Post> {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: ['comments'],
    });
    if (!post) {
      throw new NotFoundException(`文章 ID ${id} 不存在`);
    }
    return post;
  }

  // 创建文章
  async create(createPostDto: CreatePostDto): Promise<Post> {
    const post = this.postsRepository.create(createPostDto);
    return await this.postsRepository.save(post);
  }

  // 更新文章
  async update(id: number, updatePostDto: UpdatePostDto): Promise<Post> {
    const post = await this.findOne(id);
    Object.assign(post, updatePostDto);
    return await this.postsRepository.save(post);
  }

  // 删除文章
  async remove(id: number): Promise<void> {
    const post = await this.findOne(id);
    await this.postsRepository.remove(post);
  }
}
// src/comments/comments.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Comment } from './entities/comment.entity';
import { CreateCommentDto } from './dto/create-comment.dto';
import { PostsService } from '../posts/posts.service';

@Injectable()
export class CommentsService {
  constructor(
    @InjectRepository(Comment)
    private commentsRepository: Repository<Comment>,
    private postsService: PostsService,
  ) {}

  // 获取特定文章的评论
  async findByPostId(postId: number): Promise<Comment[]> {
    await this.postsService.findOne(postId); // 验证文章存在
    return await this.commentsRepository.find({ where: { postId } });
  }

  // 创建评论
  async create(createCommentDto: CreateCommentDto): Promise<Comment> {
    await this.postsService.findOne(createCommentDto.postId);
    const comment = this.commentsRepository.create(createCommentDto);
    return await this.commentsRepository.save(comment);
  }

  // 删除评论
  async remove(id: number): Promise<void> {
    const result = await this.commentsRepository.delete(id);
    if (result.affected === 0) {
      throw new BadRequestException(`评论 ID ${id} 不存在`);
    }
  }
}

更新控制器为异步操作

// src/posts/posts.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  Put,
  Delete,
  HttpCode,
  HttpStatus,
  ValidationPipe,
  ParseIntPipe,
} from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  async findAll() {
    return await this.postsService.findAll();
  }

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return await this.postsService.findOne(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body(ValidationPipe) createPostDto: CreatePostDto) {
    return await this.postsService.create(createPostDto);
  }

  @Put(':id')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body(ValidationPipe) updatePostDto: UpdatePostDto,
  ) {
    return await this.postsService.update(id, updatePostDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id', ParseIntPipe) id: number) {
    await this.postsService.remove(id);
  }
}

启动 PostgreSQL

使用 Docker Compose 快速启动 PostgreSQL:

# 先启动容器
docker run --name postgres-blog \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=password123 \
  -e POSTGRES_DB=blog_db \
  -p 5432:5432 \
  -d postgres:15-alpine

# 检查数据库连接
psql -h localhost -U postgres -d blog_db

或使用 docker-compose.yml (推荐):

# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password123
      POSTGRES_DB: blog_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

启动:

docker-compose up -d

Docker 部署

创建 Dockerfile

# Dockerfile
# 构建阶段
FROM node:18-alpine AS builder

WORKDIR /app

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 编译 TypeScript
RUN npm run build

# 运行阶段
FROM node:18-alpine

WORKDIR /app

# 从构建阶段复制编译后的代码
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./

# 暴露端口
EXPOSE 3000

# 启动应用
CMD ["node", "dist/main.js"]

创建 .dockerignore

.dockerignore
node_modules
npm-debug.log
dist
.env
.git
.gitignore
README.md
test

多容器部署(应用 + 数据库)

# docker-compose.prod.yml
version: '3.8'

services:
  # PostgreSQL 数据库
  postgres:
    image: postgres:15-alpine
    container_name: blog-postgres
    environment:
      POSTGRES_USER: ${DATABASE_USER}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: ${DATABASE_NAME}
    ports:
      - "${DATABASE_PORT}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - blog-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # NestJS 应用
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: blog-api
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_USER: ${DATABASE_USER}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      DATABASE_NAME: ${DATABASE_NAME}
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - blog-network
    restart: unless-stopped

volumes:
  postgres_data:

networks:
  blog-network:
    driver: bridge

创建生产环境 .env 文件

# .env.production
NODE_ENV=production
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=blog_db
DATABASE_USER=postgres
DATABASE_PASSWORD=your-secure-password
DATABASE_SYNCHRONIZE=false

构建和运行

# 构建 Docker 镜像
docker build -t blog-api:1.0 .

# 运行容器 (开发环境)
docker run -d \
  --name blog-api \
  -p 3000:3000 \
  -e DATABASE_HOST=host.docker.internal \
  -e DATABASE_PORT=5432 \
  -e DATABASE_NAME=blog_db \
  -e DATABASE_USER=postgres \
  -e DATABASE_PASSWORD=password123 \
  blog-api:1.0

# 或使用 docker-compose (推荐)
docker-compose -f docker-compose.prod.yml up -d

# 查看日志
docker-compose -f docker-compose.prod.yml logs -f api

# 停止服务
docker-compose -f docker-compose.prod.yml down

常用 Docker 命令

# 查看运行中的容器
docker ps

# 查看容器日志
docker logs container-name
docker logs -f container-name  # 实时查看

# 进入容器交互式 shell
docker exec -it container-name sh

# 停止/启动容器
docker stop container-name
docker start container-name

# 删除容器
docker rm container-name

# 删除镜像
docker rmi image-name

生产环境最佳实践

1. 环境变量管理

// src/config/env.ts
import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, IsString, validateSync } from 'class-validator';

enum Environment {
  Development = 'development',
  Production = 'production',
  Test = 'test',
}

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;

  @IsString()
  DATABASE_HOST: string;

  @IsNumber()
  DATABASE_PORT: number;

  @IsString()
  DATABASE_NAME: string;

  @IsString()
  DATABASE_USER: string;

  @IsString()
  DATABASE_PASSWORD: string;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToClass(EnvironmentVariables, config, {
    enableImplicitConversion: true,
  });
  const errors = validateSync(validatedConfig, {
    skipMissingProperties: false,
  });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }

  return validatedConfig;
}

2. 日志配置

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger, ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const logger = new Logger('Bootstrap');

  // 全局验证管道
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      forbidNonWhitelisted: true,
      whitelist: true,
    }),
  );

  const port = process.env.PORT || 3000;
  await app.listen(port);

  logger.log(`应用启动成功,监听端口: ${port}`);
  logger.log(`环境: ${process.env.NODE_ENV || 'development'}`);
}

bootstrap();

3. 异常处理

// src/common/filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    const errorMessage =
      typeof exceptionResponse === 'object'
        ? (exceptionResponse as any).message
        : exceptionResponse;

    this.logger.error(`${request.method} ${request.path}`, errorMessage);

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.path,
      message: errorMessage,
    });
  }
}

总结

关键要点

  1. NestJS 提供了企业级架构,特别适合大型、复杂的应用
  2. TypeScript 提供强类型检查,提高代码质量
  3. 装饰器和依赖注入 使代码更模块化、易于测试
  4. TypeORM 简化数据库操作
  5. Docker 简化部署和环境管理

常见踩坑

  • 忘记在模块中导入依赖的模块 → 导致找不到提供者
  • 不使用 DTO 验证 → 数据不安全
  • 数据库配置写死 → 不同环境无法切换
  • 忘记设置数据库关系 → 查询性能差
  • Docker 中数据库连接地址用 localhost → 跨容器通信失败

下一步学习

  1. 认证和授权 (JWT)
  2. 缓存 (Redis)
  3. 消息队列 (RabbitMQ)
  4. WebSocket 实时通信
  5. GraphQL
  6. 微服务架构
  7. 单元测试和集成测试

参考资源

祝你学习愉快!🚀

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐