07 TypeScript 与 Express 工程整合
1. 类型安装
npm install multer
npm install --save-dev @types/multer
@types/multer 会扩展 Express Request:
req.file?: Express.Multer.File;
req.files?: Express.Multer.File[] | {
[fieldname: string]: Express.Multer.File[];
};
它只是静态类型,运行时仍要检查文件是否存在、是哪种结构。
2. 单文件 Controller
import type {
Request,
Response,
} from "express";
export async function uploadAvatar(
req: Request,
res: Response,
): Promise<void> {
const file = req.file;
if (file === undefined) {
res.status(400).json({
code: "FILE_REQUIRED",
message: "请选择头像",
});
return;
}
const result = await avatarService.process({
temporaryPath: file.path,
originalName: file.originalname,
declaredMimeType: file.mimetype,
size: file.size,
});
res.status(201).json({ data: result });
}
Service 接收明确 DTO,而不是整个 Request。
3. MemoryStorage DTO
interface InMemoryUploadInput {
buffer: Buffer;
originalName: string;
declaredMimeType: string;
size: number;
}
const input: InMemoryUploadInput = {
buffer: file.buffer,
originalName: file.originalname,
declaredMimeType: file.mimetype,
size: file.size,
};
只在配置确实使用 MemoryStorage 时访问 buffer。类型声明无法保证当前 storage engine 一定设置该属性,工程中应让 upload 配置与 Controller 成对组织。
4. .array() 类型缩小
export function requireFileArray(
req: Request,
): Express.Multer.File[] {
if (!Array.isArray(req.files)) {
throw new Error("当前接口需要文件数组");
}
return req.files;
}
Controller:
const files = requireFileArray(req);
if (files.length === 0) {
res.status(400).json({ message: "至少上传一个文件" });
return;
}
5. .fields() 类型守卫
type FileMap = {
[fieldname: string]: Express.Multer.File[];
};
function isFileMap(
files: Request["files"],
): files is FileMap {
return files !== undefined && !Array.isArray(files);
}
if (!isFileMap(req.files)) {
res.status(400).json({ message: "文件字段不正确" });
return;
}
const avatar = req.files.avatar?.[0];
const gallery = req.files.gallery ?? [];
还要验证:
- avatar 是否正好一个;
- gallery 是否在允许范围;
- 是否出现意外字段;
- 每个文件真实内容是否合法。
6. 配置与路由放在一起
推荐:
routes/
document.ts
document.upload.ts
document.validation.ts
controllers/
document.controller.ts
services/
document.service.ts
middlewares/
upload-error.ts
document.upload.ts:
import multer from "multer";
export const documentUpload = multer({
storage: multer.diskStorage({
destination: "var/uploads/incoming",
}),
limits: {
fileSize: 10 * 1024 * 1024,
files: 1,
fields: 5,
parts: 6,
},
});
Multer 配置属于接口安全契约,不要在多个路由中复制不同版本的限制。
7. 认证应放在上传前
router.post(
"/documents",
requireAuthentication,
requireUploadPermission,
documentUpload.single("document"),
validateDocumentFields,
validateRequest,
uploadDocument,
);
如果先上传再鉴权,未登录用户也能消耗带宽、磁盘、内存和扫描资源。
可以在进入 Multer 前完成的检查都应提前:
- 登录状态;
- 基础权限;
- Content-Type;
- 路由级限流;
- 明显过大的 Content-Length 快速拒绝。
8. 配合 express-validator
multipart 文本字段需要先由 Multer 解析:
router.post(
"/documents",
requireAuthentication,
documentUpload.single("document"),
body("title")
.isString()
.bail()
.trim()
.isLength({ min: 1, max: 100 }),
validateRequest,
uploadDocument,
);
顺序:
Multer
→ req.body 获得 multipart 文本字段
→ express-validator
→ Controller
问题是:文本校验失败时文件已经可能落盘。需要错误处理中间件或上传 Service 清理 req.file。
9. 在上传前校验哪些文本信息
multipart 内部字段顺序不可靠,因此无法用普通 body() 中间件在 Multer 前完整验证 multipart 文本字段。
如果某些信息必须在传输文件前决定权限或目标,可以放在:
- URL params;
- query;
- 已认证用户信息;
- 独立预创建接口生成的 upload token;
- 自定义可信 Header。
例如先创建上传会话:
POST /upload-sessions
→ 校验业务参数
→ 返回 uploadSessionId
POST /upload-sessions/:id/file
→ 鉴权并上传文件
这比依赖 multipart 中晚到的 userId 更稳妥。
10. matchedData 与文件组合
interface DocumentFields {
title: string;
categoryId: number;
}
const fields = matchedData<DocumentFields>(req, {
locations: ["body"],
});
const file = req.file;
if (file === undefined) {
// 清晰处理。
}
await documentService.create({
fields,
file: {
path: file.path,
originalName: file.originalname,
declaredMimeType: file.mimetype,
size: file.size,
},
});
不要把 req.body 整体传给 Sequelize,也不要把完整 Multer File 对象直接保存为 JSON。
11. 自定义错误类型
export class UploadValidationError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly status = 400,
) {
super(message);
this.name = "UploadValidationError";
}
}
throw new UploadValidationError(
"FILE_SIGNATURE_MISMATCH",
"文件内容与声明类型不一致",
415,
);
应用错误处理中间件统一识别,不让 Controller 充满格式转换逻辑。
12. TypeScript 不会验证文件内容
const file: Express.Multer.File = req.file;
只表示对象符合类型声明,不代表:
- 文件真的是 JPEG;
- 文件没有病毒;
- 文件没有超大解压尺寸;
- originalname 安全;
- 当前用户有权限上传;
- 文件已经成功进入正式存储。
类型系统不能代替运行时安全检查。
13. Supertest 测试
import request from "supertest";
await request(app)
.post("/documents")
.field("title", "测试文档")
.field("categoryId", "1")
.attach("document", testFilePath)
.expect(201);
测试非法字段:
await request(app)
.post("/documents")
.attach("wrongField", testFilePath)
.expect(400);
测试不能只检查状态码,还应检查临时目录没有残留文件。
14. 练习题
- 安装
@types/multer,观察 Request 增加的类型。 - 为
.array()和.fields()分别编写类型守卫。 - 把当前上传 Controller 改成只向 Service 传递 DTO。
- 在 Multer 后使用 express-validator 校验 title,并处理校验失败的文件清理。
- 设计 upload session,让业务参数先于文件上传完成校验。
- 使用 Supertest 测试正常、无文件、字段错误、文件过大和残留清理。