预计时间
2 周
学习目标
- 搭建 NestJS 项目
- 用 Prisma 管理数据库
- 实现 JWT 认证 + RBAC 权限
- 文件上传到 OSS
一、为什么选 NestJS?
text
作为前端开发者,NestJS 有天然亲近感:
✅ 装饰器语法 — @Controller(), @Get(), @Post() → 类似 Angular / Spring
✅ 模块化 — Module → Controller → Service,层次清晰
✅ 依赖注入 — 开箱即用,不用自己搭
✅ TypeScript — 类型安全
✅ Swagger 自动生成 — 写完代码就有 API 文档二、项目结构
text
src/
├── main.ts # 入口:启动应用
├── app.module.ts # 根模块
├── common/ # 公共
│ ├── guards/ # 鉴权守卫
│ │ └── auth.guard.ts
│ ├── decorators/ # 自定义装饰器
│ │ └── current-user.decorator.ts
│ └── filters/ # 异常过滤器
│ └── http-exception.filter.ts
├── auth/ # 认证模块
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── dto/ # 数据传输对象
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/ # 用户模块
│ ├── users.module.ts
│ ├── users.controller.ts
│ └── users.service.ts
├── documents/ # 文档模块
│ ├── documents.module.ts
│ ├── documents.controller.ts
│ ├── documents.service.ts
│ └── dto/
└── prisma/
├── schema.prisma # 数据库模型定义
└── prisma.service.ts # Prisma 客户端封装2.1 起步
bash
npm i -g @nestjs/cli
nest new ai-saas-backend
cd ai-saas-backend
npm install @nestjs/config @prisma/client prisma
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install -D @types/passport-jwt @types/bcrypt2.2 Prisma Schema
prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Organization {
id String @id @default(uuid())
name String
slug String @unique
plan String @default("free")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
users User[]
documents Document[]
@@map("organizations")
}
model User {
id String @id @default(uuid())
organizationId String @map("organization_id")
email String @unique
passwordHash String @map("password_hash")
role String @default("member") // admin | member
createdAt DateTime @default(now()) @map("created_at")
organization Organization @relation(fields: [organizationId], references: [id])
@@map("users")
}
model Document {
id String @id @default(uuid())
organizationId String @map("organization_id")
uploadedById String @map("uploaded_by_id")
title String
fileType String @map("file_type")
fileSize BigInt @map("file_size")
fileUrl String @map("file_url")
status String @default("processing")
createdAt DateTime @default(now()) @map("created_at")
organization Organization @relation(fields: [organizationId], references: [id])
uploadedBy User @relation(fields: [uploadedById], references: [id])
@@index([organizationId, status])
@@map("documents")
}bash
npx prisma migrate dev --name init # 创建迁移
npx prisma generate # 生成类型2.3 Prisma Service
typescript
// src/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();
}
}三、JWT 认证
3.1 Auth Service
typescript
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async login(email: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) throw new UnauthorizedException('邮箱或密码错误');
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) throw new UnauthorizedException('邮箱或密码错误');
const token = this.jwtService.sign({
sub: user.id,
orgId: user.organizationId,
role: user.role,
});
return { accessToken: token };
}
async register(email: string, password: string, orgName: string) {
const passwordHash = await bcrypt.hash(password, 10);
return this.prisma.$transaction(async (tx) => {
const org = await tx.organization.create({
data: { name: orgName, slug: orgName.toLowerCase().replace(/\s+/g, '-') },
});
const user = await tx.user.create({
data: { email, passwordHash, organizationId: org.id, role: 'admin' },
});
return user;
});
}
}3.2 JWT Guard
typescript
// src/common/guards/auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// RBAC 守卫
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}3.3 使用
typescript
// src/documents/documents.controller.ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../common/guards/auth.guard';
import { RolesGuard } from '../common/guards/auth.guard';
import { Roles } from '../common/decorators/roles.decorator';
@Controller('documents')
@UseGuards(JwtAuthGuard) // 所有接口都需要登录
export class DocumentsController {
@Get()
findAll() {
// 返回当前组织的文档列表
}
@Post()
@UseGuards(RolesGuard)
@Roles('admin') // 只有 admin 能上传
upload() { }
}四、文件上传
typescript
// 安装依赖
// npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable } from '@nestjs/common';
@Injectable()
export class StorageService {
private s3 = new S3Client({
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT, // 兼容阿里云 OSS / MinIO
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
});
async uploadFile(file: Express.Multer.File, key: string) {
await this.s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
}));
return `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET}/${key}`;
}
// 生成预签名 URL(临时下载链接,60 分钟有效)
async getSignedUrl(key: string) {
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
});
return getSignedUrl(this.s3, command, { expiresIn: 3600 });
}
}五、多租户数据隔离
在 NestJS 中通过中间件 + Prisma 实现自动 tenant 隔离:
typescript
// 中间件自动注入 tenantId
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
// JWT 中取出 orgId,注入到 request
req.tenantId = req.user?.orgId;
next();
}
}
// Service 中所有查询自动带 tenantId
@Injectable()
export class DocumentsService {
constructor(private prisma: PrismaService) {}
async findAll(tenantId: string) {
return this.prisma.document.findMany({
where: { organizationId: tenantId }, // ← 强制过滤
});
}
}推荐资源
- NestJS 官方文档
- Prisma 官方文档
- 实践做一个 CRUD API:用户注册 → 登录 → 增删改查文档
实践
用 NestJS 搭一个简单的知识库 API:
nest new knowledge-api- 配置 Prisma + PostgreSQL
- 实现注册/登录(JWT)
- 实现文档 CRUD
- 加上 Swagger 文档:
npm install @nestjs/swagger→SwaggerModule.setup('api', app, document)