Remult:基于TypeScript的全栈类型安全开发框架实战指南
1. 项目概述从“全栈噩梦”到“类型安全桥梁”如果你和我一样在前后端分离架构里摸爬滚打了几年肯定对下面这个场景深恶痛绝前端写好了界面信心满满地调用一个/api/users接口结果后端返回的数据结构和你预期的User类型对不上要么多了几个字段要么少了个关键属性调试起来像在玩“大家来找茬”。更头疼的是你需要在后端定义一遍数据模型比如用 TypeORM 的 Entity在前端又得手动维护一套几乎一模一样的 TypeScript 接口一旦业务变更两边都得改稍不留神就不同步了。这种重复劳动和潜在的类型不一致我称之为“全栈开发的隐性税”。Remult 的出现就是为了彻底废除这笔“税”。它不是一个新框架而是一个极简的“粘合剂”或“类型安全桥梁”。它的核心主张是用一套 TypeScript 代码同时定义你的数据模型、业务逻辑和 API 接口。你只需要在一个地方通常是前后端共享的代码区域用 TypeScript 类定义你的实体EntityRemult 就能自动帮你完成以下工作在后端基于这个实体类自动生成对应的 RESTful API 端点并处理数据库的 CRUD 操作。在前端提供强类型的客户端库让你像调用本地对象方法一样调用 API并且获得完整的 TypeScript 类型提示和校验。简单说Remult 让你用声明式的方式定义“数据是什么”以及“能对数据做什么”它负责把这一切无缝地连接到数据库和前端界面。我第一次用它重构一个简单的后台管理项目时原本需要写几十个 API 路由和对应的前端请求函数现在只需要定义几个实体类剩下的增删改查、分页、过滤、排序全都自动有了而且前后端类型100%同步开发体验流畅得不可思议。它特别适合需要快速构建内部工具、管理后台、或者对类型安全有极高要求的全栈应用。2. 核心设计哲学约定优于配置的极致体现Remult 的设计深受“约定优于配置”Convention over Configuration原则的影响但把它提升到了类型安全的维度。它的目标不是替代你现有的后端框架如 Express、NestJS、Next.js API Routes或 ORM如 TypeORM、Prisma而是与它们优雅地集成填补类型同步的鸿沟。2.1 单一可信来源Single Source of Truth这是 Remult 的基石。在传统开发中数据模型的“真相”分散在至少三个地方数据库 Schema、后端实体/模型定义、前端类型定义。Remult 将其统一为一个用 TypeScript 装饰器Decorators修饰的实体类。这个类就是整个应用关于该数据实体的唯一权威定义。// shared/Product.ts - 这个文件可以被前后端共享 import { Entity, Fields, Validators } from remult; Entity(products, { allowApiCrud: true // 约定自动为该实体生成完整的CRUD API }) export class Product { Fields.uuid() id ; // 自动生成UUID Fields.string({ validate: Validators.required, // 声明验证规则 caption: 产品名称 // 元数据可用于生成表单标签 }) name ; Fields.number({ validate: [Validators.required, Validators.min(0)] }) price 0; Fields.boolean() inStock true; Fields.createdAt() createdAt new Date(); }这个Product类定义了字段类型、验证规则甚至一些 UI 提示信息如caption。它既是后端的实体模型也是前端的类型接口。2.2 自动化的 API 层与类型安全的客户端你不需要手动编写app.get(/api/products, handler)。Remult 在服务器端初始化时会根据实体类的allowApiCrud等配置自动注册对应的 REST API。更妙的是它会自动生成 OpenAPI 风格的元数据。在前端你通过remult客户端实例与这些 API 交互// frontend import { remult } from remult; import { Product } from ../shared/Product; async function fetchProducts() { // 类型安全的“查询构建器” const productRepo remult.repo(Product); // 查询获得类型为 Product[] 的响应 const products await productRepo.find({ where: { inStock: true }, orderBy: { price: asc }, limit: 20 }); console.log(products[0].name); // 完全的类型安全 // 新增编译器会检查字段类型 const newProduct await productRepo.insert({ name: 新商品, price: 99, inStock: true }); // 更新同样有类型校验 await productRepo.save({ ...newProduct, price: 89 }); }所有find、insert、save方法的参数和返回值都是强类型的。如果你尝试productRepo.insert({ name: 123 })TypeScript 编译器会在你写代码的时候就报错而不是等到运行时从服务器返回一个“价格必须是数字”的错误。这极大地缩短了反馈循环提升了开发效率和代码可靠性。2.3 与现有技术栈的无缝集成Remult 不强求你更换数据库或 web 框架。它通过“适配器”Adapters来连接外部世界后端框架它提供官方包可以轻松集成到 Express、Fastify、NestJS、Next.js (App Router/Pages Router)、Vite、Astro 等。数据库 ORM它主要与 TypeORM 或 Sequelize 配合利用它们的能力连接 PostgreSQL、MySQL、SQLite、SQL Server 等。你定义的 Remult 实体最终会通过这些 ORM 映射到数据库表。前端框架React、Vue、Angular、Svelte 都可以使用因为核心就是一个 TypeScript 客户端。这种设计让你可以将其渐进式地引入现有项目比如先在一个新的功能模块上试用而无需重写整个应用。3. 核心功能深度解析与实战要点理解了设计哲学我们来深入看看 Remult 如何解决具体问题。我会结合一个“任务管理应用”的实例拆解几个核心功能。3.1 实体定义不止于字段映射实体定义是 Remult 的核心。Entity和Fields装饰器提供了丰富的配置选项。实战要点字段类型与数据库映射Fields.string、Fields.number、Fields.boolean、Fields.date、Fields.json等它们不仅定义 TypeScript 类型也暗示了数据库列类型。对于Fields.jsonRemult 会自动处理序列化和反序列化。内置验证器Validators.required、Validators.min、Validators.max、Validators.unique需在服务器端配置等这些验证在前后端同时生效。前端在提交表单前即可获得即时验证反馈后端在存入数据库前会再次验证确保数据一致性。自定义验证与业务逻辑你可以在实体类中定义方法这些方法可以封装复杂的业务规则。Entity(tasks, { allowApiCrud: true }) export class Task { Fields.uuid() id ; Fields.string({ validate: Validators.required }) title ; Fields.boolean() completed false; Fields.date() dueDate?: Date; // 自定义业务逻辑方法 isOverdue() { return !this.completed this.dueDate this.dueDate new Date(); } // 静态方法用于定义服务器端逻辑 static async setCompleted(taskId: string, completed: boolean) { const taskRepo remult.repo(Task); const task await taskRepo.findId(taskId); // 这里可以添加更复杂的逻辑如权限检查、发送通知等 await taskRepo.save({ ...task, completed }); } }注意像isOverdue这样的实例方法其逻辑默认只在前端执行。如果涉及敏感数据或必须确保在服务器端执行的逻辑如setCompleted你需要通过 Remult 的“后端方法”或“实体生命周期钩子”来实现确保逻辑在受信任的服务器环境运行。3.2 查询构建器类型安全的数据库查询Remult 前端的repository.find()方法提供了一个强大的、类型安全的查询接口。它支持where、orderBy、limit、page、include等选项其语法非常直观。const taskRepo remult.repo(Task); // 复杂查询示例 const urgentTasks await taskRepo.find({ where: { completed: false, dueDate: { : new Date() } // 使用操作符 }, orderBy: { dueDate: asc }, limit: 10 }); // 分页查询 const page2 await taskRepo.find({ page: 2, pageSize: 25 });背后的原理这些查询选项会被 Remult 客户端序列化为一个特殊的查询参数通常是_后面跟一个 JSON 字符串发送到自动生成的 API 端点。服务器端的 Remult 会解析这个参数并将其转换为底层 ORM如 TypeORM的查询条件最终生成 SQL 语句。整个过程对开发者透明你得到的是类型安全的抽象。避坑技巧谨慎使用include关联加载类似 JOIN虽然方便但可能造成“N1 查询”问题或返回过大的数据量。务必清楚关联实体的数据规模和必要性。where条件的灵活性除了简单的等值匹配还支持,,,,!,in,like等操作符这为你构建复杂过滤界面提供了强大支持。服务器端过滤是黄金准则永远通过where子句在数据库层面完成数据过滤和分页而不是把所有数据取到前端再处理。这是 Remult 自动 API 的默认最佳实践。3.3 权限控制从实体到字段的精细化管理任何业务系统都绕不开权限。Remult 提供了多层次、声明式的权限控制方案这是它的一大亮点。3.3.1 实体级 API 控制在Entity装饰器中你可以精确控制哪些操作允许通过 API 访问Entity(tasks, { allowApiRead: Allow.authenticated, // 仅认证用户可读 allowApiInsert: Allow.authenticated, allowApiUpdate: (task, remult) task.createdBy remult.user.id, // 只能更新自己的任务 allowApiDelete: Allow.admin // 仅管理员可删除 }) export class Task { ... }Allow是一个工具类Allow.authenticated表示任何登录用户Allow.everyone表示所有人慎用Allow.admin是预定义角色。你也可以传入一个返回布尔值的函数实现基于数据的行级权限控制。3.3.2 字段级权限控制即使允许更新一个实体你还可以控制哪些字段可以被修改Fields.string({ allowApiUpdate: false // 该字段不允许通过API更新 }) readonlyId ; Fields.string({ allowApiUpdate: Allow.admin // 只有管理员能更新此字段 }) adminNotes ;3.3.3 服务器端验证钩子对于最复杂的业务规则你可以使用实体生命周期钩子如BackendMethod或Entity的apiPrefilter、saving等。BackendMethod({ allowed: Allow.authenticated }) static async completeTask(taskId: string, remult?: Remult) { const task await remult.repo(Task).findId(taskId); // 在这里进行任何服务器端验证和业务操作 if (task.createdBy ! remult.user.id) { throw new Error(只能完成自己的任务); } task.completed true; await remult.repo(Task).save(task); }BackendMethod定义的方法只在服务器端执行客户端调用它本质上是在调用一个特殊的 API。这是放置核心业务逻辑和最终权限检查的安全场所。实战心得权限设计应遵循“最小权限原则”。我的建议是先在实体级别用allowApiCrud: true快速原型开发然后随着功能完善逐步替换为更精细的allowApiRead、allowApiUpdate等控制最后在BackendMethod或钩子中添加核心业务规则。这样迭代起来非常顺畅。4. 完整集成实战从零构建一个 Next.js 全栈应用理论说再多不如动手搭一个。我们以最流行的全栈框架 Next.js (App Router) 为例构建一个带身份验证的任务管理器。4.1 项目初始化与依赖安装npx create-next-applatest remult-todo-app --typescript --tailwind --app cd remult-todo-app npm install remult remult-express remult/next npm install typeorm sqlite3 # 使用SQLite作为示例数据库 npm install bcryptjs jsonwebtoken types/bcryptjs types/jsonwebtoken # 认证相关4.2 定义共享实体创建shared/Task.ts和shared/User.ts// shared/Task.ts import { Entity, Fields, Allow, remult } from remult; Entity(tasks, { allowApiRead: Allow.authenticated, allowApiInsert: Allow.authenticated, allowApiUpdate: (task) task?.owner?.id remult.user?.id, allowApiDelete: (task) task?.owner?.id remult.user?.id, }) export class Task { Fields.uuid() id ; Fields.string({ validate: Validators.required }) title ; Fields.boolean() completed false; Fields.createdAt() createdAt new Date(); Fields.string() ownerId ; Fields.json({ lazy: true }) owner?: { id: string; name: string }; } // shared/User.ts import { Entity, Fields, Validators } from remult; Entity(users, { allowApiCrud: false }) // 不允许直接通过API操作用户 export class User { Fields.uuid() id ; Fields.string({ validate: Validators.required }) name ; Fields.string({ validate: Validators.required }) email ; Fields.string({ includeInApi: false }) // 密码不包含在API响应中 password ; }4.3 配置 Next.js API 路由与 Remult 服务器在app/api/[...remult]/route.ts中配置这是remult/next包约定的特殊路由// app/api/[...remult]/route.ts import { remultNextApp } from remult/next; import { Task } from ../../../shared/Task; import { User } from ../../../shared/User; import { createPostgresConnection } from typeorm; // 假设用Postgres // 获取或创建TypeORM连接 const getDbConnection async () { // ... 你的TypeORM连接配置 }; export const { POST, PUT, DELETE, GET } remultNextApp({ entities: [Task, User], getUser: async (req) { // 从请求的Cookie或Header中解析JWT获取当前用户信息 // 返回类似 { id: user-uuid, name: ..., roles: [user] } 的对象 // 这个对象会被赋值给 remult.user return await yourAuthFunction(req); }, getConnection: getDbConnection, });这个文件是 Remult 在 Next.js 中的入口所有对/api/*的请求如果匹配不到其他路由都会由 Remult 来处理并自动路由到对应实体的 CRUD 操作或后端方法。4.4 前端组件与 Remult 客户端交互在页面或组件中你需要初始化 Remult 客户端并设置认证令牌如果使用 JWT。// app/providers.tsx (一个客户端组件) use client; import { Remult, remult } from remult; import { Task } from ../shared/Task; import { useEffect } from react; export function RemultProvider({ children }: { children: React.ReactNode }) { useEffect(() { // 配置Remult实例的API地址和认证token remult.apiClient.url /api; // Next.js API路由的基础路径 const token localStorage.getItem(authToken); if (token) { remult.apiClient.setAuthToken(token); } }, []); return {children}/; } // app/page.tsx (主页面) use client; import { remult } from remult; import { Task } from ../shared/Task; import { useEffect, useState } from react; export default function HomePage() { const [tasks, setTasks] useStateTask[]([]); const [newTaskTitle, setNewTaskTitle] useState(); const taskRepo remult.repo(Task); useEffect(() { // 加载任务 taskRepo.find({ orderBy: { createdAt: desc } }).then(setTasks); }, []); const addTask async () { if (!newTaskTitle.trim()) return; const newTask await taskRepo.insert({ title: newTaskTitle, completed: false, ownerId: remult.user?.id! // 从remult.user获取当前用户ID }); setTasks([newTask, ...tasks]); setNewTaskTitle(); }; const toggleTask async (task: Task) { const updated await taskRepo.save({ ...task, completed: !task.completed }); setTasks(tasks.map(t t.id updated.id ? updated : t)); }; return ( div input value{newTaskTitle} onChange{e setNewTaskTitle(e.target.value)} / button onClick{addTask}添加/button ul {tasks.map(task ( li key{task.id} input typecheckbox checked{task.completed} onChange{() toggleTask(task)} / span{task.title}/span /li ))} /ul /div ); }4.5 身份验证流程集成身份验证是 Remult 中需要手动集成的一部分。通常流程是创建一个BackendMethod来处理登录验证用户名密码生成 JWT。前端调用这个登录方法获取 Token 并存储在本地如 localStorage 或 cookie。在 Remult 客户端设置这个 Token (remult.apiClient.setAuthToken(token))。在remultNextApp的getUser回调中验证并解析这个 Token返回用户信息。这个过程确保了 API 调用的安全性和remult.user上下文的正确性。5. 常见问题、性能考量与进阶技巧即使有了强大的抽象在实际项目中还是会遇到各种挑战。下面是我在多个项目中总结的一些经验和解决方案。5.1 性能优化与查询陷阱问题1N1 查询问题当你查询Task列表并且每个Task都想显示其所有者User的名字时如果设计不当可能会先查询 1 次获取任务列表然后对每个任务再查询 1 次获取用户信息N 次。解决方案使用 Remult 的include或 ORM 的关系映射。方法ARemultinclude在find查询中指定include。这通常会让 Remult 生成 JOIN 查询一次性获取关联数据。const tasks await taskRepo.find({ include: { owner: true } // 假设实体中定义了关系 });方法B在实体中定义关系结合底层 ORM如 TypeORM的关系装饰器ManyToOne,OneToMany并在 Remult 字段中配置。这需要更深入的 ORM 知识但更强大和灵活。问题2分页与总数计算find方法配合page和pageSize可以实现分页但如何获取总记录数以显示总页数解决方案使用repository.count()方法。const pageSize 25; const currentPage 2; const [tasks, totalCount] await Promise.all([ taskRepo.find({ page: currentPage, pageSize }), taskRepo.count() // 或者 count(whereCondition) 用于过滤后的总数 ]);注意count()是另一个独立的 API 调用。对于复杂过滤条件确保count和find使用相同的where条件。5.2 复杂业务逻辑与事务处理对于涉及多个实体修改的操作如“转账”需要保证原子性事务。解决方案在BackendMethod中使用底层 ORM 的事务管理。import { getConnection } from typeorm; BackendMethod({ allowed: Allow.authenticated }) static async transferPoints(fromUserId: string, toUserId: string, points: number, remult?: Remult) { const userRepo remult.repo(User); const connection getConnection(); // 获取TypeORM连接 await connection.transaction(async transactionalEntityManager { const fromUser await userRepo.findId(fromUserId); const toUser await userRepo.findId(toUserId); if (fromUser.points points) throw new Error(余额不足); // 使用transactionalEntityManager执行更新确保在同一个事务内 await transactionalEntityManager.update(User, fromUserId, { points: fromUser.points - points }); await transactionalEntityManager.update(User, toUserId, { points: toUser.points points }); // 还可以记录一笔交易日志... }); }将复杂的、多步骤的业务逻辑放在BackendMethod中并利用数据库事务是保证数据一致性的最佳实践。5.3 实时数据与订阅Remult 核心不直接提供 WebSocket 或 SSE 等实时功能。但对于需要实时更新的场景如聊天应用、实时仪表盘有几种整合方案轮询最简单前端定时调用repo.find()。对于更新不频繁的场景足够用使用where: { updatedAt: { : lastFetchTime } }可以优化为增量拉取。集成第三方实时服务在BackendMethod或实体生命周期钩子如saving中当数据变更时向 Pusher、Ably、Socket.io 服务器发送事件。前端订阅这些事件收到通知后再通过 Remult 拉取新数据。使用支持实时查询的数据库如果底层数据库是 Supabase 或 Firebase它们本身就提供了实时监听查询结果的功能。你可以将 Remult 与这些数据库的客户端 SDK 结合使用Remult 负责类型安全和 API 抽象数据库 SDK 负责实时流。5.4 部署与生产环境考虑API 安全性确保在生产环境中allowApiCrud: true这样的宽松设置被更严格的、基于角色的权限控制所替代。仔细审查每个实体的allowApiRead/Insert/Update/Delete规则。CORS如果你的前端和后端部署在不同域名需要在服务器端配置 CORS。使用remultExpress()或 Next.js 的中间件可以轻松配置。数据库连接池在生产环境中确保你的数据库连接如 TypeORM 连接被正确池化和管理以避免连接泄漏和性能问题。错误处理与日志Remult 抛出的错误会以标准 HTTP 错误响应返回。建议在服务器端配置全局错误处理中间件将未预期的错误记录到日志系统如 Winston、Pino并返回对用户友好的信息避免泄露堆栈跟踪。一个重要的避坑提示Remult 自动生成的 API 端点默认是公开的除非你用allowApiCrud: false或更细粒度的规则限制。在将应用部署到公网前务必使用remult.user和权限规则对所有敏感数据端点进行保护。对于管理类操作强烈建议使用BackendMethod因为其执行逻辑完全在服务器端控制之下更为安全。从我个人的使用体验来看Remult 最适合的场景是开发速度至关重要、且团队熟悉 TypeScript 的中小型全栈项目尤其是内部工具、管理后台和原型开发。它能将你从繁琐的胶水代码中解放出来让你更专注于核心业务逻辑。然而对于超大规模、需要极度定制化 API 或复杂微服务架构的应用Remult 的“约定”可能会成为一种限制需要评估其灵活性是否满足要求。但对于绝大多数场景它提供的生产力和类型安全保障是极具吸引力的。