本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 把
UsersService从“直接操作数组”升级为“依赖仓储接口”。 - 引入
UsersRepository抽象,默认使用内存实现。 - 为后续切换数据库(Prisma/TypeORM)做无痛准备。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
2.1 为什么要引入 Repository 抽象
Service 的职责是业务规则,不应该绑定具体存储细节。把存储抽象成仓储接口后:
- 业务层与存储层解耦;
- 测试可以替换为 mock/fake;
- 换数据库时,Controller 和大部分业务逻辑不用动。
2.2 Provider Token 是什么
接口在运行时不存在,所以 Nest 不能直接按接口注入。要用一个 Token(字符串或 Symbol)作为注入标识。
export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
2.3 useClass 的作用
useClass 表示“当有人注入这个 Token 时,用哪个具体类实例化”。
{
provide: USERS_REPOSITORY,
useClass: InMemoryUsersRepository,
}
2.4 设计注意点
- Token 建议用
Symbol,避免命名冲突; - Service 依赖 Token,不依赖具体类;
- 模块中统一管理绑定关系,避免散落在业务代码里。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
3.1 src/users/users.repository.ts - 定义 Token 与仓储契约
export const USERS_REPOSITORY = Symbol('USERS_REPOSITORY');
export type UserRole = 'user' | 'moderator' | 'admin';
export interface UserEntity {
id: number;
username: string;
email: string;
passwordHash: string;
role: UserRole;
createdAt: Date;
}
export interface UsersRepository {
create(data: Omit<UserEntity, 'id' | 'createdAt'>): Promise<UserEntity>;
findAll(): Promise<UserEntity[]>;
findById(id: number): Promise<UserEntity | null>;
findByEmail(email: string): Promise<UserEntity | null>;
updateRole(id: number, role: UserRole): Promise<UserEntity | null>;
}
3.2 src/users/repositories/in-memory-users.repository.ts - 默认实现
import { Injectable } from '@nestjs/common';
import { UserEntity, UserRole, UsersRepository } from '../users.repository';
@Injectable()
export class InMemoryUsersRepository implements UsersRepository {
private readonly users: UserEntity[] = [];
private nextId = 1;
async create(data: Omit<UserEntity, 'id' | 'createdAt'>): Promise<UserEntity> {
const user: UserEntity = { id: this.nextId++, createdAt: new Date(), ...data };
this.users.push(user);
return user;
}
async findAll(): Promise<UserEntity[]> {
return [...this.users];
}
async findById(id: number): Promise<UserEntity | null> {
return this.users.find((user) => user.id === id) ?? null;
}
async findByEmail(email: string): Promise<UserEntity | null> {
return this.users.find((user) => user.email === email) ?? null;
}
async updateRole(id: number, role: UserRole): Promise<UserEntity | null> {
const user = this.users.find((item) => item.id === id);
if (!user) return null;
user.role = role;
return user;
}
}
3.3 src/users/users.module.ts - 绑定 Token 与实现
import { Module } from '@nestjs/common';
import { InMemoryUsersRepository } from './repositories/in-memory-users.repository';
import { UsersController } from './users.controller';
import { USERS_REPOSITORY } from './users.repository';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [
UsersService,
InMemoryUsersRepository,
{
provide: USERS_REPOSITORY,
useClass: InMemoryUsersRepository,
},
],
exports: [UsersService, USERS_REPOSITORY],
})
export class UsersModule {}
3.4 为什么这里会同时出现“类 Token”和“自定义 Token”
这里是最容易疑惑的一段代码。先看结论:两者都可以存在,但职责不同。
- 类 Token(
InMemoryUsersRepository)用于“按具体类注入”; - 自定义 Token(
USERS_REPOSITORY)用于“按抽象契约注入”。
所以在 providers 里同时注册它们,是为了兼容两种注入方式。
但如果写成 useClass: InMemoryUsersRepository,Nest 会按两个 Token 分别实例化,可能产生双实例。
如果你希望两种注入方式共享同一个仓储实例,建议改为:
@Module({
providers: [
UsersService,
InMemoryUsersRepository,
{
provide: USERS_REPOSITORY,
useExisting: InMemoryUsersRepository,
},
],
})
export class UsersModule {}
3.5 src/users/users.service.ts - 注入抽象而非具体类
import { Inject, Injectable } from '@nestjs/common';
import { USERS_REPOSITORY } from './users.repository';
import type { UsersRepository } from './users.repository';
@Injectable()
export class UsersService {
constructor(
@Inject(USERS_REPOSITORY)
private readonly usersRepository: UsersRepository,
) {}
findAll() {
return this.usersRepository.findAll();
}
}
3.6 本节落地价值
当你后面换成 Prisma 仓储时,只需要替换 useClass 绑定,Service 层几乎不用改。这就是可演进架构的核心收益。
4. 总结这节课知识点
- 你学会了用 Token + Provider 绑定做“依赖可替换”。
- 你理解了类 Token 与自定义 Token 可以并存,以及如何避免双实例。
- 你把业务和存储彻底解耦,后续扩展成本大幅下降。
- 你掌握了中大型 Nest 项目最关键的结构能力之一。
5. 每节课分支
lesson-03-repository-pattern- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git