Nestjs搭建中后台系统-后端
ps:搭配前端项目一起使用
前端
非专业后端,只是对Nestjs感兴趣,可以一起讨论。
仓库地址
新建项目
nest new app system
依赖安装完后后,进入根目录,执行npm run start:dev

使用apifox试一下。
这里调试请求的工具推荐apifox,国产的,很好用。
https://apifox.com/
apifox建一个团队然后一个项目,进入项目,请求成功。

然后我们就要给它接入很多功能了。就像一个人,打扮的要漂漂亮亮的对吧,换句话说,工欲善其事,必先利其器。
接入pgsql
我们使用pgsql作为数据库。
工具的话,使用prisma。
prisma文档。
安装pnpm install prisma --save-dev
根目录新建一个文件.env
c
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
mydb改成你的数据库名,localhost:5432改成你的远程的,johndoe为用户名,randompassword为密码。
根目录新建一个文件 prisma/schema.prisma
c
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
这个时候你可能要装个vscode插件

这个时候,如果你的数据库地址正确,基本上就可以使用数据库了,但是我们要有表对吧。
写一个model-User
c
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @unique @default(uuid())
email String? @unique
name String?
username String? @unique
password String?
}
是不是和Ts很像,可选?。为什么都标记可选?有可能email登陆,也有可能username/password登陆。
同步到数据库。
之前写过一篇数据库连接使用的
npx prisma migrate dev --name init

这个时候可以发现

现在看看你的数据库是不是多了表结构。可以使用pgAdmin看看

然后查看下这个sql文件

详细的定义,表结构,关联,主键等等可以看这个数据库连接使用的
到此数据库基础接入就完成了,表结构的定义也有了,但是我们是不是还要写一遍ts类型定义?现在其实是有插件,可以直接生成的。
社区插件

pnpm install prisma-class-generator --save-dev
安装 pnpm install @prisma/client等下要用。
初始化自己的prisma库
nest g lib prisma

在src/prisma/schema.prisma增加代码生成提示
c
generator client {
provider = "prisma-client-js"
}
generator prismaClassGenerator {
provider = "prisma-class-generator"
output = "../libs/prisma/src/class"
dryRun = false
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @unique @default(uuid())
email String? @unique
name String?
username String? @unique
password String?
}
执行npx prisma generate
这个时候,你会发现,你的libs/prisma/src下面多了个目录。
还贴心的帮我们引入了swagger

如果你不想要,看配置。这个要看自己需求了,如果你用swagger,就不用处理。



完善 libs/prisma/src/prisma.service/ts
ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
这个时候,在你想要用的地方引入prisma模块就可以了,为了方便使用,你也可以加上@Global。

错误处理
对数据库的异常处理。
以前nestjs系列里面应该写过一个错误处理。
ts
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common';
import {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
} from '@prisma/client/runtime/library';
import { Request, Response } from 'express';
@Catch(
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
)
export class ErrorFilter<T> implements ExceptionFilter {
catch(
exception:
| PrismaClientKnownRequestError
| PrismaClientUnknownRequestError
| PrismaClientRustPanicError
| PrismaClientInitializationError
| PrismaClientValidationError,
host: ArgumentsHost,
) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let message = exception.message;
let time = Date.now();
let code;
let meta;
let reqDesc =
`[<${time}>-${request.method}-${request.url}]` +
JSON.stringify(request.body);
if (exception instanceof PrismaClientKnownRequestError) {
code = exception.code;
meta = exception.meta;
Logger.error(getPrismaErrorMessage(code, JSON.stringify(meta)), reqDesc);
message = '数据库操作异常';
}
if (exception instanceof PrismaClientUnknownRequestError) {
message = '未知查询错误';
}
if (exception instanceof PrismaClientRustPanicError) {
message = '数据库运行异常';
}
if (exception instanceof PrismaClientInitializationError) {
code = exception.errorCode;
Logger.error(getPrismaErrorMessage(code, message));
message = '数据库连接异常';
}
if (exception instanceof PrismaClientValidationError) {
message = '数据结构异常';
}
// throw new CustomException(code, message, 400);
response.status(400).json({
code: code,
message: message,
time,
});
}
}
export function getPrismaErrorMessage(code: string, message?: string): string {
const errorMessages: Record<string, string> = {
P1000: '数据库身份验证失败',
P1001: '无法访问数据库服务器',
P1002: '连接数据库超时',
P1003: '数据库文件不存在',
P1004: '数据库架构不存在',
P1005: '数据库不存在',
P1008: '操作超时',
P1009: '数据库已存在',
P1010: '访问数据库被拒绝',
P1011: 'TLS 连接错误',
P1012: 'Prisma 架构验证失败',
P1013: '无效的数据库连接字符串',
P1014: '底层数据库对象不存在',
P1015: '数据库版本不支持当前功能',
P1016: '原始查询参数数量错误',
P1017: '服务器已关闭连接',
// 新增错误码 (P2000-P2037)
P2000: '字段值长度超出限制',
P2001: '查询记录不存在',
P2002: '唯一约束冲突',
P2003: '外键约束失败',
P2004: '数据库约束失败',
P2005: '字段值类型无效',
P2006: '提供的字段值无效',
P2007: '数据验证错误',
P2008: '查询解析失败',
P2009: '查询验证失败',
P2010: '原始查询执行失败',
P2011: '违反非空约束',
P2012: '缺少必需值',
P2013: '缺少字段参数',
P2014: '违反必需关系约束',
P2015: '相关记录未找到',
P2016: '查询解释错误',
P2017: '关系记录未关联',
P2018: '连接记录未找到',
P2019: '输入参数错误',
P2020: '值超出类型范围',
P2021: '数据库表不存在',
P2022: '数据库列不存在',
P2023: '列数据不一致',
P2024: '连接池获取连接超时',
P2025: '依赖的记录不存在',
P2026: '数据库不支持此功能',
P2027: '数据库发生多个错误',
P2028: '事务 API 错误',
P2029: '查询参数超出限制',
P2030: '缺少全文索引',
P2031: 'MongoDB 需要副本集运行',
P2033: '数字超出 64 位整数范围',
P2034: '事务因冲突失败,请重试',
P2035: '数据库断言违规',
P2036: '外部连接器错误',
P2037: '打开的数据库连接过多',
};
const baseMessage = errorMessages[code] || ``;
let prefix = code ? `[${code}]` : '';
if (code && code.startsWith('P1')) {
prefix = prefix + '数据库连接异常: ';
}
if (code && code.startsWith('P2')) {
prefix = prefix + '数据库内部异常: ';
}
const _message = message ? `${baseMessage}: ${message}` : baseMessage;
return prefix + _message;
}
lib-prisma新建一个文件,先导入它,后面用。连接的时候也可以处理下。


index.ts导出下

接入Redis
nest g lib redis

安装redis pnpm install redis -save
在libs/redis/src/redis.service.ts
ts
import {
Injectable,
OnModuleInit,
OnModuleDestroy,
Logger,
} from '@nestjs/common';
import { createClient, RedisClientType } from 'redis';
@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
private readonly client: RedisClientType;
private readonly logger = new Logger(RedisService.name);
constructor() {
this.client = createClient({
url: process.env.REDIS_URL,
password: process.env.REDIS_PASSWORD,
});
}
async onModuleInit() {
await this.client.connect();
this.client.on('error', (err) => this.logger.error('Redis服务异常r', err));
}
async onModuleDestroy() {
await this.client.quit();
}
get(key: string) {
return this.client.get(key);
}
set(key: string, value: string, options?: { ttl: number }) {
if (options?.ttl) {
return this.client.setEx(key, options.ttl, value);
}
return this.client.set(key, value);
}
del(key: string) {
return this.client.del(key);
}
getClient(): RedisClientType {
return this.client;
}
// 按需添加更多方法...
}
在.env完善信息

拆分微服务
用户服务
nest g app user
日志服务
nest g app logger
邮件推送服务
nest g app email
都执行完成后得到下面的结构。

日志服务我们放到最后再介入,和mongodb一起,作为可选项。
邮件服务
邮件服务,主要负责向用户发送邮件,如验证码,通知等等。
主要围绕apps/email,因为这类服务是不需要阻塞主进程的完成的,可以使用kafka。之前有一篇文章写过,使用nacos和kafka。
我们可以照搬引入。
公共库nacos
nest g lib nacos
同样的在.env写入配置
安装nacos pnpm install nacos --save

在libs/nacos/src/nacos.module.ts和nacos.service.ts完善逻辑
需要安装@nestjs/config来读取env数据 pnpm install @nestjs/config --save-dev
service.ts
ts
import {
Inject,
Injectable,
Logger,
OnApplicationBootstrap,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { NacosNamingClient } from 'nacos';
import { ConfigService } from '@nestjs/config';
interface Instance {
instanceId: string;
ip: string; //IP of instance
port: number; //Port of instance
healthy: boolean;
enabled: boolean;
serviceName?: string;
weight?: number;
ephemeral?: boolean;
clusterName?: string;
}
export interface NacosOptions {
serviceName: string; //Service name
instance: Partial<Instance>; //Instance
groupName?: string;
}
@Injectable()
export class NacosService implements OnApplicationBootstrap, OnModuleDestroy {
@Inject('CONFIG_OPTIONS')
private options: NacosOptions;
@Inject(ConfigService)
private configService: ConfigService;
private readonly logger = new Logger(NacosService.name);
private client: NacosNamingClient;
async onApplicationBootstrap() {
this.client = new NacosNamingClient({
logger: {
...console,
...this.logger,
},
serverList: this.configService.get<string>('NACOS_SERVER'), // replace to real nacos serverList
namespace: this.configService.get<string>('NACOS_NAMESPACE'),
username: this.configService.get<string>('NACOS_SECRET_NAME'),
password: this.configService.get<string>('NACOS_SECRET_PWD'),
});
await this.client.ready();
await this.register();
this.logger.log('Nacos客户端准备就绪');
}
getClient(): NacosNamingClient {
return this.client;
}
async register() {
await this.client.registerInstance(
this.options.serviceName,
// @ts-ignore
this.options.instance,
this.options.groupName,
);
}
async destroy() {
await this.client.deregisterInstance(
this.options.serviceName,
// @ts-ignore
this.options.instance,
this.options.groupName,
);
}
async onModuleDestroy() {
await this.destroy();
}
}
module.ts
ts
import { DynamicModule, Module } from '@nestjs/common';
import { NacosOptions, NacosService } from './nacos.service';
import { ConfigModule } from '@nestjs/config';
@Module({})
export class NacosModule {
static forRoot(options: NacosOptions): DynamicModule {
return {
imports: [
ConfigModule.forRoot({
envFilePath: ['.env'],
}),
],
module: NacosModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
NacosService,
],
exports: [NacosService],
};
}
}
引入使用nacos库
在email服务
email.module.ts
ts
import { Module } from '@nestjs/common';
import { EmailController } from './email.controller';
import { EmailService } from './email.service';
import { NacosModule } from '@app/nacos';
@Module({
imports: [
NacosModule.forRoot({
serviceName: 'email',
instance: {
ip: '0.0.0.0',
port: Number(process.env.EMAIL_PORT),
},
}),
],
controllers: [EmailController],
providers: [EmailService],
})
export class EmailModule {}
这个时候启动email服务npm run start:dev email

当然也可以在env配置EMAIL_IP,EMAIL_PORT,或者使用nacos的配置管理。
nacos库新增nacos-config.service.ts
并且在nacos.module中引入
ts
import {
Inject,
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosConfigClient } from 'nacos';
export interface ConfigOptions {
defaultConfigList: string[];
}
@Injectable()
export class NacosConfigService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(NacosConfigService.name);
private client: NacosConfigClient;
@Inject()
private configService: ConfigService;
constructor() {}
async onModuleInit() {
try {
this.client = new NacosConfigClient({
serverAddr: this.configService.get<string>('NACOS_SERVER'),
namespace: this.configService.get<string>('NACOS_NAMESPACE'),
username: this.configService.get<string>('NACOS_SECRET_NAME'),
password: this.configService.get<string>('NACOS_SECRET_PWD'),
});
this.logger.log('Nacos配置客户端初始化成功');
} catch (error) {
this.logger.error('Nacos配置客户端初始化失败', error);
}
}
async onModuleDestroy() {
await this.client.close();
}
async getConfig(dataId: string, group = 'DEFAULT_GROUP') {
const _dataId = `naocs_config_${dataId}`;
if (this.configService.get(_dataId)) {
return await this.configService.get(_dataId);
}
const config = this.parseConfig(
await this.client.getConfig(dataId, group),
'json',
);
this.configService.set(_dataId, config);
return config;
}
/**
* 解析配置内容
*/
private parseConfig(content: string, type: string): any {
try {
if (type === 'json') {
return JSON.parse(content);
} else if (type === 'yaml' || type === 'yml') {
// 简单的YAML解析,实际项目中可以使用js-yaml等库
const config = {};
content.split('\n').forEach((line) => {
const parts = line.split(':').map((part) => part.trim());
if (parts.length >= 2) {
config[parts[0]] = parts.slice(1).join(':');
}
});
return config;
} else if (type === 'properties') {
const config = {};
content.split('\n').forEach((line) => {
const parts = line.split('=').map((part) => part.trim());
if (parts.length >= 2) {
config[parts[0]] = parts.slice(1).join('=');
}
});
return config;
}
return content;
} catch (error) {
this.logger.error('配置解析失败', error);
return content;
}
}
getLocalConfig(dataId: string) {
return this.configService.get(dataId);
}
}
nacos-config.module.ts
ts
import { Module } from '@nestjs/common';
import { NacosConfigService } from './nacos-config.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env'],
}),
],
providers: [NacosConfigService],
exports: [NacosConfigService],
})
export class NacosConfigModule {}
index.ts导出

比如我们的email要拿nacos的配置,假设实例名称为email_one

在nacos配置那边新增配置

在邮件服务main.ts
ts
import { NestFactory } from '@nestjs/core';
import { EmailModule } from './email.module';
import { Logger } from '@nestjs/common';
import { NacosConfigService, NacosConfigModule } from '@app/nacos';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const configApp = await NestFactory.create(NacosConfigModule);
await configApp.init();
const nacosConfigService =
configApp.get<NacosConfigService>(NacosConfigService);
const emailName = nacosConfigService.getLocalConfig('RUN_NAME');
const res = await nacosConfigService.getConfig(emailName);
if (!res.port) {
throw new Error('邮件服务配置错误');
}
const configName = `nacos_config_${emailName}`;
const port = res.port;
const app = await NestFactory.create(EmailModule);
const configService = app.get<ConfigService>(ConfigService);
configService.set(configName, res);
await app.listen(port);
Logger.log(`邮件服务已经启动 ${port}`);
await configApp.close();
}
bootstrap();
这个时候configService是可以拿到nacos_xxx的了。启动的端口也是在nacos配置的。
对应的nacos的引入也要变个方式。



同时服务也注册到了nacos。

到此nacos的配置中心和注册服务完成了。
引入kafka
Kafka的也类似。
安装依赖 pnpm install kafkajs --save
pnpm install @nestjs/microservices --save
注意版本号,和@nestjs其他一致,不然可能会报错。
我们不是在main.ts拿到配置了吗。
配置里面也可以注入kafka的配置,将配置细化。

刚刚的nacos改一下

然后email的启动方式也修改下。
main.ts
ts
import { NestFactory } from '@nestjs/core';
import { EmailModule } from './email.module';
import { Logger } from '@nestjs/common';
import { NacosConfigService, NacosConfigModule } from '@app/nacos';
import { ConfigService } from '@nestjs/config';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
async function bootstrap() {
const configApp = await NestFactory.create(NacosConfigModule);
await configApp.init();
const nacosConfigService =
configApp.get<NacosConfigService>(NacosConfigService);
const emailName = nacosConfigService.getLocalConfig('RUN_NAME');
const res = await nacosConfigService.getConfig(emailName);
if (!res) {
throw new Error('邮件服务配置错误');
}
const configName = `nacos_config_${emailName}`;
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
EmailModule,
{
transport: Transport.KAFKA,
options: {
client: {
brokers: res.kafka.brokers,
},
consumer: {
groupId: res.kafka.groupId,
},
},
},
);
const configService = app.get<ConfigService>(ConfigService);
configService.set(configName, res);
await app.listen();
Logger.log(`邮件服务已经启动`);
await configApp.close();
}
bootstrap();

在其他服务调用。
比如用户服务。
为了公用RUN_NAME
可以在 启动服务时注入。使用cross-env

这样开发环境使用配置就ok了。
邮件发送
email.service.ts
ts
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createTransport } from 'nodemailer';
export interface EmailOptions {
subject: string;
text?: string;
html?: string;
}
@Injectable()
export class EmailService implements OnModuleInit {
private transporter;
@Inject(ConfigService)
private readonly configService: ConfigService;
private config;
async onModuleInit() {
const name = this.configService.get<string>('RUN_NAME');
this.config = this.configService.get(`nacos_config_${name}`);
this.transporter = createTransport(this.config.email);
}
sendEmail(data: EmailOptions) {
this.transporter.sendMail({
from: `"${this.config.email.name}" <${this.config.email.auth.user}>`,
to: this.config.email.auth.user,
...data,
});
}
}
email.controller.ts
ts
import { Controller, Get } from '@nestjs/common';
import { EmailOptions, EmailService } from './email.service';
import { MessagePattern, Payload } from '@nestjs/microservices';
@Controller()
export class EmailController {
constructor(private readonly emailService: EmailService) {}
@MessagePattern('sendEmail')
sendEmail(@Payload() data: EmailOptions) {
this.emailService.sendEmail(data);
}
}
用户服务
nacos新增配置

用户微服务,我们使用TPC,不使用kafka,用户服务偏向于同步,更适合。

main.ts
ts
import { NestFactory } from '@nestjs/core';
import { UserModule } from './user.module';
import { NacosConfigModule, NacosConfigService } from '@app/nacos';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const configApp = await NestFactory.create(NacosConfigModule);
await configApp.init();
const nacosConfigService =
configApp.get<NacosConfigService>(NacosConfigService);
const name = nacosConfigService.getLocalConfig('RUN_NAME');
const res = await nacosConfigService.getConfig(name);
if (!res.run) {
throw new Error('用户服务配置错误');
}
const configName = `nacos_config_${name}`;
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
UserModule,
{
transport: Transport.TCP,
options: {
host: res.run.ip,
port: res.run.port,
},
},
);
const configService = app.get<ConfigService>(ConfigService);
configService.set(configName, res);
await app.listen();
Logger.log(`用户服务已经启动`);
await configApp.close();
}
bootstrap();
启动


实现用户创建



System服务
作为网关服务,引入使用用户服务。
比如我们在system src 目录下,新增一个 user的资源

nest g res user

安装pnpm install @nestjs/mapped-types --save-dev
给nacos.service.ts增加方法

调用



加上邮件服务
在应用user中,调用邮件kafka
ts
import { PrismaService } from '@app/prisma';
import {
Inject,
Injectable,
Logger,
OnApplicationBootstrap,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ClientKafka } from '@nestjs/microservices';
@Injectable()
export class UserService implements OnApplicationBootstrap {
@Inject(PrismaService)
private readonly prismaService: PrismaService;
private emailClient: ClientKafka;
@Inject(ConfigService)
private readonly configService: ConfigService;
async onApplicationBootstrap() {
const name = this.configService.get<string>('RUN_NAME');
const config = this.configService.get(`nacos_config_${name}`);
this.emailClient = new ClientKafka({
client: {
brokers: config.kafka.brokers,
},
producer: {
allowAutoTopicCreation: true,
},
});
await this.emailClient.connect();
Logger.log(`email-service 连接成功`);
}
async registerByUserName(username: string, password: string) {
const res = await this.prismaService.user.create({
data: {
username,
password,
},
});
this.emailClient.emit('sendEmail', {
subject: '注册成功',
text: `用户名: ${username} 密码: ${password}`,
});
return res;
}
getHello(): string {
return 'Hello World!';
}
}
调用


改造-pgsql和redis接入nacos配置
之前我们的pgsql和redis的接入,都是通过env,现在我们也改造下。
nacos的user_one配置,新增数据库配置。


改造-pgsql


读取到nacos配置后,重新传入下url而不是读.env。
但是我们在本地开发的时候,还是要走固定开发环境路径的,不过无所谓,反正都丢给nacos配置。

但是本地开发环境如果你使用prisma做数据库操作的话尽量放.env,但是你也可以写个脚本,读取下nacos配置,再调用prisma数据库迁移指令。
到这里获取配置,其实也可以抽离成一个服务,后续优化再说。功能优先。
改造Redis
同上一样。



到此,我们的所有配置都丢到了nacos。项目启动的时候,.env通过注入Nacos的连接配置,然后获取nacos的应用端口,kafka等的配置注入到configService。好比docker启动项目的话,只用在环境变量里面加入nacos配置以及应用RUN_NAME即可。
这样的话,我们部署多实例的时候,可以从第三方,比如我们写一个实例管理的页面,连接nacos配置管理,新增配置,然后调用后端脚本启动一个docker,启动后通过查看nacos是否有该实例判断是否启动成功等等。
更新中
2025-10-31
完成基本搭建,nacos,kafka,Redis和PgSql的接入。下一步开始完善用户服务,以及统一响应处理,mongodb引入和日志系统,并实现简单docker部署。
权限设计
2025-11-01思考的。
用户和角色,多对多。
角色和权限,多对多。
在页面上,管理员可以配置路由权限,菜单,按钮等等的权限,页面加载的时候通过获取配置来加载路由,本地不写死路由。