全栈开发后端实践项目:博客系统后端(NestJS + TypeORM + JWT + GraphQL)

本项目将深入演示后端开发的核心概念:依赖注入(DI)、模块化架构、RESTful API 设计、GraphQL 集成、JWT 身份认证、授权守卫、异常处理、数据库操作等。通过实现一个完整的博客系统后端,展示如何构建健壮、可测试、高内聚低耦合的服务端应用。


一、UML 建模(Mermaid)

1. 系统组件架构图

外部服务

NestJS应用

客户端

基础设施层

数据访问层

业务逻辑层

表示层

Web/Mobile App

REST Controllers

GraphQL Resolvers

认证服务

用户服务

文章服务

TypeORM Repository

JWT模块

守卫 Guard

拦截器

异常过滤器

PostgreSQL

Redis缓存

2. 认证流程序列图(JWT + Refresh Token)

PostService Redis JWTService UserRepository AuthService AuthController Client PostService Redis JWTService UserRepository AuthService AuthController Client alt [验证成功] [验证失败] 后续请求携带 AccessToken AccessToken 过期,使用 RefreshToken 刷新 alt [refreshToken有效] [无效] POST /auth/login (email, password) validateUser(email, password) findOneByEmail(email) user (hashed password) compare passwords generateAccessToken(user) accessToken generateRefreshToken(user) refreshToken store refreshToken (key: userId) { accessToken, refreshToken } 200 OK 401 Unauthorized 401 GET /posts (Authorization: Bearer accessToken) verify(token) decoded payload getPosts(userId) posts 200 OK POST /auth/refresh (refreshToken) refresh(refreshToken) get(refreshToken对应的userId) generateAccessToken(user) newAccessToken { accessToken } 200 401 401

3. 数据模型 ER 图

authors

writes

has

USER

int

id

PK

string

email

UK

string

passwordHash

string

username

datetime

createdAt

datetime

updatedAt

POST

int

id

PK

string

title

string

content

string

slug

UK

datetime

publishedAt

int

authorId

FK

datetime

createdAt

datetime

updatedAt

COMMENT

int

id

PK

string

content

int

postId

FK

int

userId

FK

datetime

createdAt

4. 核心模块类图(简化)

User

+id: number

+email: string

+passwordHash: string

+username: string

+createdAt: Date

+updatedAt: Date

+posts: Post[]

Post

+id: number

+title: string

+content: string

+slug: string

+publishedAt: Date

+authorId: number

+author: User

+comments: Comment[]

Comment

+id: number

+content: string

+postId: number

+userId: number

+user: User

+post: Post

AuthService

+validateUser(email, password)

+login(user)

+refresh(refreshToken)

+logout(userId)

JwtStrategy

+validate(payload)

JwtAuthGuard

+canActivate(context)

UsersService

+create(createUserDto)

+findAll()

+findOne(id)

+findByEmail(email)

PostsService

+create(createPostDto, userId)

+findAll()

+findOne(id)

+update(id, updatePostDto)

+remove(id)

UsersController

+getProfile(req)

+create(createUserDto)

+findAll()

PostsController

+create(createPostDto, req)

+findAll()

+findOne(id)

+update(id, updatePostDto)

+remove(id)

AuthController

+login(loginDto)

+refresh(refreshTokenDto)

+logout(req)

Redis


二、项目文件结构

blog-backend/
├── src/
│   ├── main.ts                       # 应用入口
│   ├── app.module.ts                 # 根模块
│   ├── common/                       # 通用组件
│   │   ├── decorators/               # 自定义装饰器
│   │   │   ├── public.decorator.ts   # 跳过认证的标记
│   │   │   └── current-user.decorator.ts
│   │   ├── filters/                  # 异常过滤器
│   │   │   └── http-exception.filter.ts
│   │   ├── guards/                   # 守卫
│   │   │   └── jwt-auth.guard.ts
│   │   ├── interceptors/             # 拦截器
│   │   │   └── transform.interceptor.ts
│   │   └── pipes/                    # 管道
│   │       └── validation.pipe.ts
│   ├── config/                       # 配置模块
│   │   ├── configuration.ts
│   │   └── validation.ts
│   ├── modules/                      # 业务模块
│   │   ├── auth/                     # 认证模块
│   │   │   ├── auth.controller.ts
│   │   │   ├── auth.service.ts
│   │   │   ├── auth.module.ts
│   │   │   ├── dto/
│   │   │   │   ├── login.dto.ts
│   │   │   │   └── refresh-token.dto.ts
│   │   │   └── strategies/
│   │   │       ├── jwt.strategy.ts
│   │   │       └── local.strategy.ts
│   │   ├── users/                    # 用户模块
│   │   │   ├── users.controller.ts
│   │   │   ├── users.service.ts
│   │   │   ├── users.module.ts
│   │   │   ├── dto/
│   │   │   │   ├── create-user.dto.ts
│   │   │   │   └── update-user.dto.ts
│   │   │   └── entities/
│   │   │       └── user.entity.ts
│   │   ├── posts/                    # 文章模块
│   │   │   ├── posts.controller.ts
│   │   │   ├── posts.service.ts
│   │   │   ├── posts.module.ts
│   │   │   ├── dto/
│   │   │   │   ├── create-post.dto.ts
│   │   │   │   └── update-post.dto.ts
│   │   │   └── entities/
│   │   │       └── post.entity.ts
│   │   └── comments/                 # 评论模块
│   │       ├── comments.module.ts
│   │       ├── comments.service.ts
│   │       ├── comments.resolver.ts  # GraphQL 解析器
│   │       └── entities/
│   │           └── comment.entity.ts
│   ├── graphql/                      # GraphQL 相关
│   │   ├── graphql.module.ts
│   │   └── scalars/
│   │       └── date.scalar.ts
│   └── database/                     # 数据库配置
│       ├── database.module.ts
│       └── database.providers.ts
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── nest-cli.json
├── package.json
├── tsconfig.json
└── README.md

三、核心源代码实现

1. 环境配置与根模块

.env.example

# 应用配置
PORT=3000
NODE_ENV=development

# 数据库
DB_TYPE=postgres
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=blog

# JWT
JWT_SECRET=your-secret-key
JWT_ACCESS_TOKEN_TTL=15m
JWT_REFRESH_TOKEN_TTL=7d

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

src/config/configuration.ts

export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  database: {
    type: process.env.DB_TYPE || 'postgres',
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    username: process.env.DB_USERNAME || 'postgres',
    password: process.env.DB_PASSWORD || 'postgres',
    database: process.env.DB_DATABASE || 'blog',
    synchronize: process.env.NODE_ENV !== 'production',
    logging: process.env.NODE_ENV === 'development',
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    accessTokenTtl: process.env.JWT_ACCESS_TOKEN_TTL || '15m',
    refreshTokenTtl: process.env.JWT_REFRESH_TOKEN_TTL || '7d',
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
  },
});

src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RedisModule } from '@nestjs-modules/ioredis';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { PostsModule } from './modules/posts/posts.module';
import { CommentsModule } from './modules/comments/comments.module';
import { GraphQLModule } from './graphql/graphql.module';
import configuration from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => config.get('database'),
    }),
    RedisModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        config: {
          host: config.get('redis.host'),
          port: config.get('redis.port'),
        },
      }),
    }),
    GraphQLModule,
    AuthModule,
    UsersModule,
    PostsModule,
    CommentsModule,
  ],
})
export class AppModule {}

2. 实体与数据库

src/modules/users/entities/user.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { Exclude } from 'class-transformer';
import { Post } from '../../posts/entities/post.entity';
import { Comment } from '../../comments/entities/comment.entity';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  email: string;

  @Column()
  @Exclude() // 防止返回密码哈希
  passwordHash: string;

  @Column()
  username: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @OneToMany(() => Post, (post) => post.author)
  posts: Post[];

  @OneToMany(() => Comment, (comment) => comment.user)
  comments: Comment[];
}

src/modules/posts/entities/post.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  ManyToOne,
  OneToMany,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Comment } from '../../comments/entities/comment.entity';

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

  @Column()
  title: string;

  @Column('text')
  content: string;

  @Column({ unique: true })
  slug: string;

  @Column({ nullable: true })
  publishedAt: Date;

  @ManyToOne(() => User, (user) => user.posts)
  author: User;

  @Column()
  authorId: number;

  @OneToMany(() => Comment, (comment) => comment.post)
  comments: Comment[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

src/modules/comments/entities/comment.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  ManyToOne,
  CreateDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Post } from '../../posts/entities/post.entity';

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

  @Column('text')
  content: string;

  @ManyToOne(() => User, (user) => user.comments)
  user: User;

  @Column()
  userId: number;

  @ManyToOne(() => Post, (post) => post.comments)
  post: Post;

  @Column()
  postId: number;

  @CreateDateColumn()
  createdAt: Date;
}

3. 用户模块(体现 DI 与 CRUD)

src/modules/users/dto/create-user.dto.ts

import { IsEmail, IsString, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty()
  @IsEmail()
  email: string;

  @ApiProperty()
  @IsString()
  @MinLength(6)
  password: string;

  @ApiProperty()
  @IsString()
  username: string;
}

src/modules/users/users.service.ts

import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    const existing = await this.usersRepository.findOne({
      where: { email: createUserDto.email },
    });
    if (existing) {
      throw new ConflictException('Email already exists');
    }

    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
    const user = this.usersRepository.create({
      ...createUserDto,
      passwordHash: hashedPassword,
    });
    return this.usersRepository.save(user);
  }

  async findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOne({ where: { id } });
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.usersRepository.findOne({ where: { email } });
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);
    if (updateUserDto.password) {
      updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
    }
    Object.assign(user, updateUserDto);
    return this.usersRepository.save(user);
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
  }
}

src/modules/users/users.controller.ts(展示 RESTful API)

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  UseGuards,
  Req,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from './entities/user.entity';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  @UseGuards(JwtAuthGuard)
  findAll() {
    return this.usersService.findAll();
  }

  @Get('profile')
  @UseGuards(JwtAuthGuard)
  getProfile(@CurrentUser() user: User) {
    return user;
  }

  @Get(':id')
  @UseGuards(JwtAuthGuard)
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Patch(':id')
  @UseGuards(JwtAuthGuard)
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

4. 认证模块(JWT + Refresh Token + Redis)

src/modules/auth/dto/login.dto.ts

import { IsEmail, IsString } from 'class-validator';

export class LoginDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

src/modules/auth/auth.service.ts

import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Redis } from 'ioredis';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { User } from '../users/entities/user.entity';
import { LoginDto } from './dto/login.dto';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
    @Inject('REDIS_CLIENT') private redisClient: Redis,
  ) {}

  async validateUser(email: string, password: string): Promise<User | null> {
    const user = await this.usersService.findByEmail(email);
    if (user && (await bcrypt.compare(password, user.passwordHash))) {
      return user;
    }
    return null;
  }

  async login(loginDto: LoginDto) {
    const user = await this.validateUser(loginDto.email, loginDto.password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload = { sub: user.id, email: user.email };
    const accessToken = this.jwtService.sign(payload, {
      expiresIn: process.env.JWT_ACCESS_TOKEN_TTL || '15m',
    });
    const refreshToken = this.jwtService.sign(payload, {
      expiresIn: process.env.JWT_REFRESH_TOKEN_TTL || '7d',
    });

    // 存储 refreshToken 到 Redis,键为 refreshToken:userId,设置过期时间(7天)
    await this.redisClient.set(
      `refresh:${user.id}`,
      refreshToken,
      'EX',
      7 * 24 * 60 * 60,
    );

    return {
      accessToken,
      refreshToken,
      user: { id: user.id, email: user.email, username: user.username },
    };
  }

  async refresh(refreshToken: string) {
    try {
      const payload = this.jwtService.verify(refreshToken);
      const storedToken = await this.redisClient.get(`refresh:${payload.sub}`);
      if (storedToken !== refreshToken) {
        throw new UnauthorizedException('Invalid refresh token');
      }

      const user = await this.usersService.findOne(payload.sub);
      const newPayload = { sub: user.id, email: user.email };
      const newAccessToken = this.jwtService.sign(newPayload, {
        expiresIn: process.env.JWT_ACCESS_TOKEN_TTL || '15m',
      });

      return { accessToken: newAccessToken };
    } catch (err) {
      throw new UnauthorizedException('Refresh token expired or invalid');
    }
  }

  async logout(userId: number) {
    await this.redisClient.del(`refresh:${userId}`);
    return { message: 'Logged out successfully' };
  }
}

src/modules/auth/strategies/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('jwt.secret'),
    });
  }

  async validate(payload: any) {
    return { id: payload.sub, email: payload.email };
  }
}

src/modules/auth/auth.controller.ts

import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }

  @Post('refresh')
  refresh(@Body() refreshTokenDto: RefreshTokenDto) {
    return this.authService.refresh(refreshTokenDto.refreshToken);
  }

  @Post('logout')
  @UseGuards(JwtAuthGuard)
  logout(@Req() req) {
    return this.authService.logout(req.user.id);
  }
}

src/common/guards/jwt-auth.guard.ts(展示守卫)

import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    if (err || !user) {
      throw err || new UnauthorizedException('Unauthorized');
    }
    return user;
  }
}

src/common/decorators/public.decorator.ts(允许跳过认证)

import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

5. 文章模块(RESTful 完整实现)

src/modules/posts/dto/create-post.dto.ts

import { IsString, IsNotEmpty, IsOptional, IsDateString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreatePostDto {
  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  title: string;

  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  content: string;

  @ApiProperty()
  @IsString()
  @IsNotEmpty()
  slug: string;

  @ApiProperty({ required: false })
  @IsDateString()
  @IsOptional()
  publishedAt?: Date;
}

src/modules/posts/posts.service.ts

import { Injectable, NotFoundException, ForbiddenException } 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 create(createPostDto: CreatePostDto, authorId: number): Promise<Post> {
    const post = this.postsRepository.create({
      ...createPostDto,
      authorId,
    });
    return this.postsRepository.save(post);
  }

  async findAll(): Promise<Post[]> {
    return this.postsRepository.find({ relations: ['author'] });
  }

  async findOne(id: number): Promise<Post> {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: ['author', 'comments', 'comments.user'],
    });
    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
    return post;
  }

  async update(id: number, updatePostDto: UpdatePostDto, userId: number): Promise<Post> {
    const post = await this.findOne(id);
    if (post.authorId !== userId) {
      throw new ForbiddenException('You are not the author of this post');
    }
    Object.assign(post, updatePostDto);
    return this.postsRepository.save(post);
  }

  async remove(id: number, userId: number): Promise<void> {
    const post = await this.findOne(id);
    if (post.authorId !== userId) {
      throw new ForbiddenException('You are not the author of this post');
    }
    await this.postsRepository.delete(id);
  }
}

src/modules/posts/posts.controller.ts(展示 RESTful API 与认证)

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  UseGuards,
  Req,
} from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { Public } from '../../common/decorators/public.decorator';

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

  @Post()
  @UseGuards(JwtAuthGuard)
  create(@Body() createPostDto: CreatePostDto, @Req() req) {
    return this.postsService.create(createPostDto, req.user.id);
  }

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

  @Get(':id')
  @Public()
  findOne(@Param('id') id: string) {
    return this.postsService.findOne(+id);
  }

  @Patch(':id')
  @UseGuards(JwtAuthGuard)
  update(
    @Param('id') id: string,
    @Body() updatePostDto: UpdatePostDto,
    @Req() req,
  ) {
    return this.postsService.update(+id, updatePostDto, req.user.id);
  }

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  remove(@Param('id') id: string, @Req() req) {
    return this.postsService.remove(+id, req.user.id);
  }
}

6. GraphQL 集成示例(评论模块)

src/modules/comments/comments.resolver.ts

import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { Comment } from './entities/comment.entity';
import { CreateCommentInput } from './dto/create-comment.input';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';

@Resolver(() => Comment)
export class CommentsResolver {
  constructor(private commentsService: CommentsService) {}

  @Query(() => [Comment])
  async commentsByPost(@Args('postId') postId: number) {
    return this.commentsService.findByPost(postId);
  }

  @Mutation(() => Comment)
  @UseGuards(JwtAuthGuard)
  async createComment(
    @Args('input') input: CreateCommentInput,
    @Context('req') req,
  ) {
    return this.commentsService.create(input, req.user.id);
  }
}

src/modules/comments/dto/create-comment.input.ts

import { InputType, Field } from '@nestjs/graphql';
import { IsString, IsInt } from 'class-validator';

@InputType()
export class CreateCommentInput {
  @Field()
  @IsString()
  content: string;

  @Field()
  @IsInt()
  postId: number;
}

src/graphql/graphql.module.ts

import { Module } from '@nestjs/common';
import { GraphQLModule as NestGraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    NestGraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/graphql/schema.gql'),
      sortSchema: true,
      playground: process.env.NODE_ENV !== 'production',
      context: ({ req }) => ({ req }),
    }),
  ],
})
export class GraphQLModule {}

7. 全局异常过滤器与拦截器

src/common/filters/http-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const res = exception.getResponse();
      message = typeof res === 'string' ? res : (res as any).message;
    } else if (exception instanceof Error) {
      message = exception.message;
    }

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

src/main.ts(应用入口,应用全局过滤器)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.useGlobalFilters(new AllExceptionsFilter());
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

四、深入解析

1. 依赖注入(DI)与模块化

  • NestJS 的 DI 机制:通过 @Injectable() 装饰器将类标记为可注入,在模块的 providers 中注册。例如 UsersServiceUsersModule 中提供,然后在 UsersController 中通过构造函数注入。这实现了松耦合,便于单元测试(可模拟 UsersService)。
  • 模块化:每个业务模块(如 UsersModule)封装了该领域的控制器、服务、实体等,并通过 exports 共享服务给其他模块。根模块 AppModule 导入所有子模块,形成清晰的依赖树。

2. AOP(面向切面编程)实现

  • 守卫(Guard)JwtAuthGuard 在请求到达控制器前验证 JWT,实现了权限控制。结合 @Public() 装饰器,可以灵活跳过认证。
  • 拦截器(Interceptor):可定义 TransformInterceptor 统一格式化响应数据(如添加 data 字段、去除 passwordHash)。
  • 过滤器(Filter)AllExceptionsFilter 捕获所有异常,统一返回结构化的错误响应,避免暴露内部堆栈。

3. RESTful API 设计

  • 资源定义/users, /posts, /comments 作为资源。
  • HTTP 动词
    • GET /posts – 获取列表(幂等)
    • POST /posts – 创建新资源(非幂等)
    • PATCH /posts/:id – 部分更新(非幂等,但可设计为幂等)
    • DELETE /posts/:id – 删除(幂等)
  • 状态码
    • 201 Created – 创建成功
    • 200 OK – 成功响应
    • 400 Bad Request – 验证错误
    • 401 Unauthorized – 未认证
    • 403 Forbidden – 无权限
    • 404 Not Found – 资源不存在

4. JWT 与 Refresh Token 机制

  • Access Token:短期有效(如15分钟),携带用户身份,用于API访问。
  • Refresh Token:长期有效(如7天),存储在服务端(Redis)并返回给客户端。客户端在 Access Token 过期后使用 Refresh Token 获取新的 Access Token,避免频繁登录。
  • 安全考虑
    • Access Token 放在 Authorization: Bearer 头中传输。
    • Refresh Token 存储时使用 HttpOnly Cookie 或移动端安全存储,此处示例中通过 JSON 返回,实际生产环境建议使用 httpOnly Cookie 防止 XSS。
    • Redis 中存储 Refresh Token,支持服务端主动撤销(如用户登出或修改密码时)。

5. GraphQL 解决 Over-fetching 与 N+1

  • Schema 定义:通过 @ObjectType()@Field() 定义 GraphQL 类型,自动生成 schema。
  • ResolverCommentsResolver 提供查询和变更。GraphQL 允许客户端指定所需字段,避免了 REST 中的过度获取。
  • N+1 问题:若在解析评论时嵌套获取用户信息,可能导致 N+1 查询。解决方案是使用 DataLoader 批量加载。可在 CommentsService 中实现 DataLoader 来优化。

6. 数据库与 TypeORM

  • 实体关系UserPost 一对多,PostComment 一对多。TypeORM 通过装饰器建立映射。
  • 索引:在 emailslug 等字段上可添加索引以提升查询性能。
  • 事务:可使用 @Transaction()QueryRunner 确保数据一致性(例如创建文章时同时创建标签)。

7. 测试策略

  • 单元测试:使用 Jest 测试服务层,模拟 Repository。
  • 集成测试:使用 @nestjs/testing 创建测试模块,验证控制器与数据库交互。
  • E2E 测试:使用 supertest 测试完整 API 流程。

五、总结

本后端项目通过 NestJS 框架系统性地实践了全栈开发中后端领域的核心知识:依赖注入、模块化架构、RESTful API 设计、JWT 认证、Redis 会话管理、GraphQL 集成、异常处理、数据库操作

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐