全栈开发(二)后端开发:业务逻辑与数据处理
全栈开发后端实践项目:博客系统后端(NestJS + TypeORM + JWT + GraphQL)
本项目将深入演示后端开发的核心概念:依赖注入(DI)、模块化架构、RESTful API 设计、GraphQL 集成、JWT 身份认证、授权守卫、异常处理、数据库操作等。通过实现一个完整的博客系统后端,展示如何构建健壮、可测试、高内聚低耦合的服务端应用。
一、UML 建模(Mermaid)
1. 系统组件架构图
2. 认证流程序列图(JWT + Refresh Token)
3. 数据模型 ER 图
4. 核心模块类图(简化)
二、项目文件结构
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中注册。例如UsersService在UsersModule中提供,然后在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 存储时使用
HttpOnlyCookie 或移动端安全存储,此处示例中通过 JSON 返回,实际生产环境建议使用httpOnlyCookie 防止 XSS。 - Redis 中存储 Refresh Token,支持服务端主动撤销(如用户登出或修改密码时)。
- Access Token 放在
5. GraphQL 解决 Over-fetching 与 N+1
- Schema 定义:通过
@ObjectType()和@Field()定义 GraphQL 类型,自动生成 schema。 - Resolver:
CommentsResolver提供查询和变更。GraphQL 允许客户端指定所需字段,避免了 REST 中的过度获取。 - N+1 问题:若在解析评论时嵌套获取用户信息,可能导致 N+1 查询。解决方案是使用 DataLoader 批量加载。可在
CommentsService中实现 DataLoader 来优化。
6. 数据库与 TypeORM
- 实体关系:
User与Post一对多,Post与Comment一对多。TypeORM 通过装饰器建立映射。 - 索引:在
email、slug等字段上可添加索引以提升查询性能。 - 事务:可使用
@Transaction()或QueryRunner确保数据一致性(例如创建文章时同时创建标签)。
7. 测试策略
- 单元测试:使用 Jest 测试服务层,模拟 Repository。
- 集成测试:使用
@nestjs/testing创建测试模块,验证控制器与数据库交互。 - E2E 测试:使用
supertest测试完整 API 流程。
五、总结
本后端项目通过 NestJS 框架系统性地实践了全栈开发中后端领域的核心知识:依赖注入、模块化架构、RESTful API 设计、JWT 认证、Redis 会话管理、GraphQL 集成、异常处理、数据库操作。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)