从 schema 设计到 JWT 登录,一个 TypeScript 后端的“降维打击”
技术栈:NestJS 10 + TypeScript + Prisma 6 + PostgreSQL + JWT + class-validator
🧠 开篇:当后端开始写“类”,DBA 开始慌了
“你这数据库关系这么复杂——用户、文章、评论、标签、点赞、头像、文件……至少要三天建表吧?”
“不用,我写个 schema.prisma,一条命令搞定。”
“……你是不是偷偷请了 DBA?”
在 NestJS + Prisma 的世界里,数据库不再是黑盒,而是 TypeScript 类的延伸。
User 表?就是一个 model User {}。
多对多标签?PostTag 联结表自动生成。
连外键约束、索引、字段映射,都能用装饰器搞定。
今天,就带你看看这套“现代化后端流水线”是如何让 CRUD 变成艺术的。
🗃️ 一、Prisma Schema:数据库的“TypeScript 设计稿”
传统方式:先建库 → 再建表 → 再写 Model → 再写 Migration。
Prisma 方式:先写 schema,再一键生成一切。
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
content String? @db.Text
user User? @relation(fields: [userId], references: [id])
userId Int?
tags PostTag[]
likes UserLikesPost[]
@@index([userId])
@@map("posts")
}
这段代码,既是文档,又是代码,还是数据库蓝图。
@relation自动处理外键@@map("posts")映射真实表名(避免复数歧义)@db.VarChar(255)精确控制字段类型@@index([userId])自动生成数据库索引
为什么 Prisma 比 TypeORM 更香?
| 对比项 | TypeORM | Prisma |
|---|---|---|
| 查询语法 | 链式方法 | 函数式 + include |
| 类型推导 | 弱 | 强(自动生成 TS 类型) |
| 迁移 | 手动或 auto | 完全可控 |
| 性能 | 中等 | 极快(查询构建器优化) |
| 学习曲线 | 陡峭 | 平缓 |
💡 真实案例:我们有一个“获取文章详情 + 作者 + 标签 + 点赞数”的接口。
用 Prisma 一行 include 搞定:this.prisma.post.findUnique({ where: { id }, include: { user: { select: { name: true } }, tags: { include: { tag: true } }, likes: { select: { userId: true } } } });而 TypeORM 需要写 3 个 left join + 手动去重——Prisma 把复杂留给自己,把简单留给开发者。
初始化流程(三步走)
-
创建数据库
CREATE DATABASE notes_ai WITH OWNER=postgres ENCODING='UTF8'; -
初始化 Prisma
npx prisma init # 生成 .env + prisma/schema.prisma -
迁移 & 生成 Client
npx prisma migrate dev --name init-user npx prisma generate
从此,@prisma/client 会根据 schema 自动生成类型安全的查询 API——连字段拼错都会被 TS 编译器拦下!
🧪 二、DTO + ValidationPipe:参数校验,优雅到骨子里
前端传个 page=abc?直接 400 报错。
多传了个 hackField?自动过滤(whitelist: true)。
少传必填字段?立刻拦截(forbidNonWhitelisted: true)。
这一切,靠的是 class-validator + class-transformer:
// dto/post-query.dto.ts
import { IsOptional, IsInt, Min, Max, Type } from 'class-validator';
export class PostQueryDto {
@IsOptional()
@Type(() => Number) // 自动 string → number
@IsInt({ message: 'page 必须是整数' })
@Min(1, { message: 'page 至少为 1' })
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Max(100, { message: 'limit 不能超过 100' })
limit?: number = 10;
}
在 main.ts 中启用全局管道:
// main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 自动转换类型
whitelist: true, // 删除非 DTO 定义的属性
forbidNonWhitelisted: true, // 非白名单字段直接报错
validationError: { target: false } // 隐藏内部类名,保护隐私
})
);
await app.listen(3000);
}
从此,控制器方法签名干净如诗:
// posts.controller.ts
@Get()
findAll(@Query() query: PostQueryDto) {
// query.page 已是 number,无需 parseInt!
return this.postsService.findAll(query);
}
🤯 反常识:最好的错误处理,是在请求进入业务逻辑前就拦截掉。
用户看到的不是“服务器错误”,而是“page 必须是整数”——这就是专业。
🔌 三、PrismaService:依赖注入的极致优雅
NestJS 的依赖注入系统,让数据库客户端像自来水一样随处可用:
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient
implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
// prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
@Global() // 全局模块,无需重复导入
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
然后在任何 Service 中直接注入:
// posts/posts.service.ts
@Injectable()
export class PostsService {
constructor(private prisma: PrismaService) {}
async findAll(query: PostQueryDto) {
const { page, limit } = query;
const skip = (page - 1) * limit;
const [total, data] = await Promise.all([
this.prisma.post.count(),
this.prisma.post.findMany({
skip,
take: limit,
include: {
user: { select: { name: true } },
tags: { include: { tag: true } }
}
})
]);
return {
list: data,
pagination: { total, page, limit, totalPages: Math.ceil(total / limit) }
};
}
}
启动时自动连接,退出时自动断开——这才是企业级应用该有的样子。
🔐 四、JWT 登录:无状态认证,安全又轻量
HTTP 是无状态的,所以我们用 JWT:
// auth/auth.service.ts
import * as bcrypt from 'bcrypt';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService) {}
async login(username: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { name: username } });
if (!user || !bcrypt.compareSync(password, user.password)) {
throw new UnauthorizedException('用户名或密码错误');
}
const payload = { sub: user.id, username: user.name };
return {
access_token: jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '7d' })
};
}
}
前端每次请求,Axios 拦截器自动注入 token:
// 前端 axios.interceptors.request.use(...)
// headers.Authorization = `Bearer ${token}`
NestJS 中间件验证(简化版):
// auth/auth.middleware.ts
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ message: '未登录' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
(req as any).user = payload; // 挂载到 request
next();
} catch {
return res.status(401).json({ message: 'Token 无效' });
}
}
}
身份信息直接嵌入 token,免查库(适合非敏感场景)。
如需更高安全,可搭配 Redis 做 token 黑名单(登出时加入黑名单)。
🏗️ 五、工程化思维:从 seeds 到日志,打造完整闭环
1. Seeds:初始化测试数据
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
await prisma.user.create({
data: {
name: 'admin',
password: bcrypt.hashSync('123456', 10)
}
});
}
main()
.catch(e => console.error(e))
.finally(() => prisma.$disconnect());
运行 npx prisma db seed,一键填充初始数据。
2. 环境隔离
.env 文件管理配置:
# .env
DATABASE_URL="postgresql://postgres:123456@localhost:5432/notes_ai"
JWT_SECRET="your-super-secret-jwt-key"
配合 config 模块,不同环境(dev/staging/prod)自动切换。
3. 日志与监控
- 开发环境:Prisma 打印 SQL(
log: ['query']) - 生产环境:集成 Winston 日志 + Sentry 错误追踪
- 健康检查:用
@nestjs/terminus提供/health接口
🎯 结语:后端,也可以很“前端”
谁说后端必须枯燥?
用 TypeScript 写数据库模型,用装饰器定义 API,用管道校验参数——每一行代码,都是对混乱的反抗。
NestJS + Prisma,不只是工具链,更是一种工程美学。
它让后端开发者,也能享受“类型安全”、“模块化”、“可测试”的现代开发乐趣。
💡 最后暴言:当你用 Prisma 写出
post.user.avatars.filename这种链式查询时,SQL 老炮儿可能会流泪——但你的产品经理会笑。
因为——你交付的不是接口,是确定性。