Remult框架:全栈TypeScript开发中模型驱动与类型安全的新范式
1. 项目概述从“又一个框架”到“全栈开发的新范式”如果你和我一样在过去几年里被全栈开发中前后端数据模型同步、API接口定义、权限验证这些重复且易错的“脏活累活”折磨得够呛那么看到remult/remult这个项目时你的第一反应可能和我当初一样“哦又一个试图解决这些问题的框架。” 但当我真正把它引入到一个中型SaaS项目中并完整地走完一个开发周期后我才意识到它远不止是一个框架更像是一种全新的开发范式。它用一种极其巧妙的方式将后端的数据模型、业务逻辑与前端的数据访问、状态管理无缝地编织在了一起让你感觉像是在写一个“大一统”的应用而不是在费力地粘合两个独立的系统。简单来说Remult 是一个用于全栈 TypeScript/JavaScript 应用的开源框架。它的核心价值主张是用一套 TypeScript 代码同时定义你的数据模型、后端 API 以及前端的数据访问客户端。你不再需要手动编写 RESTful 或 GraphQL 接口的路径、请求方法、参数和响应体也不再需要在前端手动创建对应的请求函数和状态管理。Remult 帮你自动生成了这一切并且是强类型、可扩展的。它不是一个“大而全”的解决方案而是专注于解决全栈开发中最核心、最繁琐的“数据流”问题。它适合那些厌倦了在前后端之间反复横跳、复制粘贴、调试接口不一致的开发者尤其是那些已经拥抱 TypeScript 并追求开发效率和代码质量的团队。2. 核心设计哲学模型即一切Remult 的魔力源于一个非常清晰且坚定的设计哲学模型驱动开发。这里的“模型”不是指 UML 图而是你的业务实体Entity在代码中的真实映射。在 Remult 的世界里你首先用 TypeScript 的类Class和装饰器Decorators来定义一个实体。2.1 一个实体三重定义让我们从一个最经典的Task任务实体开始看看 Remult 是如何工作的。// shared/Task.ts import { Entity, Fields, Validators } from remult; Entity(tasks, { allowApiCrud: true // 允许对该实体进行增删改查的API操作 }) export class Task { Fields.uuid() id ; Fields.string({ validate: Validators.required, caption: 任务标题 // 前端表单的标签 }) title ; Fields.boolean() completed false; Fields.createdAt() createdAt new Date(); }就这么一段代码Remult 为你做了三件大事数据库模型定义Entity和Fields装饰器定义了数据库表或集合tasks的结构。Remult 内置了与多种 ORM如 TypeORM、Prisma或直接数据库驱动如 SQL的适配器可以根据这个定义自动创建或同步表结构。后端 API 定义通过allowApiCrud: true等配置Remult 自动为这个Task实体生成一套完整的、安全的 RESTful API。你立刻拥有了GET /api/tasks,POST /api/tasks,PUT /api/tasks/:id,DELETE /api/tasks/:id等端点并且支持过滤、排序、分页等高级查询。前端类型安全的客户端在前端你可以直接导入这个Task类并使用 Remult 提供的repo仓库对象进行数据操作。所有的操作都是强类型的IDE 可以提供完美的自动补全和错误提示。2.2 前后端类型共享的终极体验这是 Remult 最令人愉悦的一点。由于模型定义在前后端共享的代码中通常是shared/目录类型安全贯穿了整个数据流。后端业务逻辑可以直接使用这个Task类// backend/taskController.ts import { Task } from ../shared/Task.js; import { BackendMethod, remult } from remult; export class TasksController { BackendMethod({ allowed: true }) static async setAllCompleted(completed: boolean) { const taskRepo remult.repo(Task); // 这里的所有操作都是类型安全的 for (const task of await taskRepo.find()) { await taskRepo.save({ ...task, completed }); } } }前端组件也使用完全相同的类型// frontend/TodoList.tsx import { useEffect, useState } from react; import { remult } from remult; import { Task } from ../../shared/Task; const taskRepo remult.repo(Task); function TodoList() { const [tasks, setTasks] useStateTask[]([]); useEffect(() { // 查询所有未完成的任务并按照创建时间排序 // 这里的 where 和 orderBy 都是类型安全的 taskRepo.find({ where: { completed: false }, orderBy: { createdAt: asc } }).then(setTasks); }, []); const addTask async (title: string) { // 创建新任务IDE会提示你需要 title 属性 const newTask await taskRepo.insert({ title }); setTasks([...tasks, newTask]); }; // ... 渲染逻辑 }你会发现我们不需要手动定义任何 API 路由不需要写axios或fetch调用也不需要为 API 响应定义额外的 TypeScript 接口。Task这个类就是唯一的真相来源。当你修改实体字段时比如给Task增加一个priority字段前后端的类型错误会同时暴露出来这种开发体验是革命性的。实操心得将所有的实体定义放在一个前后端都能访问的目录如shared/是 Remult 的最佳实践。这通常意味着你需要配置一个 Monorepo如使用 Turborepo、Nx或者一个简单的符号链接。对于全栈 TypeScript 项目这几乎是零成本的但带来的类型安全收益是巨大的。3. 深入核心不仅仅是 CRUD如果 Remult 只是自动生成 CRUD API那它可能还不足以让人惊艳。它的强大之处在于在提供这种极致便利的同时并没有牺牲灵活性和控制力。它通过几个核心概念让你能够精细地控制业务逻辑。3.1 实体元数据Metadata与验证Validation装饰器不仅定义了结构还承载了丰富的元数据。例如Fields.string({ validate: Validators.required, caption: “任务标题” })。这些元数据会被 Remult 的运行时和前端框架充分利用。前端表单生成像caption这样的信息可以被 UI 库如 Remult 的 React 插件用来自动生成表单标签实现简单的快速原型开发。后端验证Validators.required等验证规则会在 API 请求到达你的业务逻辑之前自动执行。这意味着无效的数据根本不会进入你的save或insert方法大大减少了业务代码中的防御性检查。自定义验证你可以轻松定义自己的验证规则。Fields.stringTask({ validate: (task) { if (task.title.length 100) { throw new Error(“标题不能超过100个字符”); } } }) title “”;3.2 细粒度的 API 权限控制自动生成 API 很酷但安全是重中之重。Remult 提供了从实体级别到行级别的、声明式的权限控制。实体级别在Entity装饰器中控制。Entity(“tasks”, { allowApiRead: Allow.authenticated, // 仅认证用户可读 allowApiInsert: Allow.authenticated, allowApiUpdate: (task, remult) task.createdByUserId remult.user.id, // 只能更新自己的任务 allowApiDelete: Allow.admin // 仅管理员可删除 })字段级别你可以控制哪些字段可以通过 API 暴露。Fields.string({ allowApiUpdate: false }) // 该字段不允许通过API更新 internalCode “”;行级别通过apiPrefilter实现。例如让用户只能看到和操作自己的任务。Entity(“tasks”, { apiPrefilter: () ({ createdByUserId: remult.user.id }) })这个过滤器会自动注入到所有查询中无论是通过 API 还是后端直接调用repo.find()用户都只能访问到符合条件的数据。这极大地简化了多租户SaaS或用户数据隔离场景下的开发。3.3 后端方法Backend Methods处理复杂业务逻辑并非所有操作都是简单的 CRUD。对于复杂的、涉及多个实体或需要特定权限的业务流程Remult 提供了BackendMethod装饰器。// shared/TasksController.ts import { BackendMethod, remult } from “remult”; import { Task } from “./Task.js”; export class TasksController { BackendMethod({ allowed: Allow.authenticated }) static async calculateProjectStats(projectId: string) { const taskRepo remult.repo(Task); const tasks await taskRepo.find({ where: { projectId } }); // 复杂的统计计算逻辑... return { total: tasks.length, completed: tasks.filter(t t.completed).length, // ... 其他统计信息 }; } }在前端你可以像调用本地函数一样调用它Remult 会自动处理网络请求。// 前端调用 const stats await TasksController.calculateProjectStats(“proj_123”);这种方式将 RPC远程过程调用的简洁性与类型安全完美结合用于处理那些不适合映射为 RESTful 资源操作的功能。4. 实战配置与集成指南理解了核心概念后让我们看看如何在一个真实的全栈项目以 Next.js Prisma 为例中搭建 Remult。4.1 项目初始化与依赖安装首先创建一个标准的 Next.js 项目App Router并安装必要依赖。npx create-next-applatest my-remult-app --typescript --tailwind --app cd my-remult-app npm install remult remult-react # 根据选择的数据库ORM安装适配器这里以Prisma为例 npm install prisma prisma/client remult/adapters-prisma npm install -D prisma4.2 配置 Remult 服务端Next.js API Routes在 Next.js 的app/api/[...remult]/route.ts中创建 Remult API 路由处理器。这是连接 Remult 和 Next.js 的关键。// app/api/[...remult]/route.ts import { remultNextApp } from “remult/remult-next”; import { createKnexDataProvider } from “remult/adapters-knex”; // 假设用Knex // 或 import { PrismaClient } from “prisma/client”; // import { createPrismaDataProvider } from “remult/adapters-prisma”; // 1. 导入你的实体 import { Task } from “../../../shared/Task”; // 2. 导入你的控制器 import { TasksController } from “../../../shared/TasksController”; // 3. 创建数据提供者以内存为例生产环境需连接真实DB let dataProvider createMemoryDataProvider(); // 如果用 Prisma // const prisma new PrismaClient(); // const dataProvider createPrismaDataProvider(prisma); // 4. 创建并导出 API 处理器 export const api remultNextApp({ entities: [Task], // 注册实体 controllers: [TasksController], // 注册控制器 dataProvider, // 提供数据源 // 配置用户身份验证例如从 NextAuth 会话中获取 getUser: async (req) { const session await getServerSession(authOptions); // NextAuth 示例 return session?.user; } }); export const { POST, PUT, DELETE, GET, PATCH } api;这个文件创建了一个“万能”API 路由 (/api/[...remult])所有对注册实体和控制器的请求都会由 Remult 自动路由和处理。4.3 配置 Remult 客户端在前端应用的最顶层例如app/providers.tsx你需要初始化 Remult 客户端并使其在整个 React 树中可用。// app/providers.tsx “use client”; import { RemultReact } from “remult-react”; import { remult } from “remult”; // 配置 Remult 客户端实例 remult.apiClient.url “/api”; // 指向我们刚刚创建的 Next.js API 路由 export function Providers({ children }: { children: React.ReactNode }) { return ( RemultReact.Provider {children} /RemultReact.Provider ); }然后在app/layout.tsx中使用这个 Provider。4.4 在组件中使用 Remult现在你可以在任何客户端组件中使用useRemult钩子或直接使用remult.repo来操作数据了。// app/page.tsx “use client”; import { useEffect, useState } from “react”; import { useRemult } from “remult-react”; import { Task } from “../shared/Task”; export default function HomePage() { const remult useRemult(); const [tasks, setTasks] useStateTask[]([]); const taskRepo remult.repo(Task); const loadTasks async () { // 类型安全的查询 const taskList await taskRepo.find({ orderBy: { createdAt: “desc” }, limit: 20 }); setTasks(taskList); }; useEffect(() { loadTasks(); }, []); return ( div h1任务列表/h1 ul {tasks.map(task ( li key{task.id}{task.title} - {task.completed ? “✅” : “⏳”}/li ))} /ul /div ); }4.5 集成数据库以 Prisma 为例初始化 Prismanpx prisma init这会在项目根目录创建prisma/schema.prisma文件。根据 Remult 实体定义 Prisma Schema// prisma/schema.prisma generator client { provider “prisma-client-js” } datasource db { provider “postgresql” // 或 sqlite, mysql 等 url env(“DATABASE_URL”) } model Task { id String id default(uuid()) title String completed Boolean default(false) createdAt DateTime default(now()) map(“tasks”) // 映射到数据库中的 “tasks” 表 }注意字段名和类型与Task实体类保持一致。生成并迁移数据库npx prisma db push # 或使用迁移工具 # npx prisma migrate dev --name init在 Remult API 路由中使用 Prisma 数据提供者 修改之前的app/api/[...remult]/route.ts将内存数据提供者替换为 Prisma 提供者。注意事项在生产环境中要特别注意数据库连接的管理。在 Serverless 环境如 Vercel中需要确保 Prisma Client 实例是单例的或者使用连接池。Remult 的适配器通常能很好地处理这些细节但了解底层原理有助于排查性能问题。5. 高级特性与最佳实践当项目规模增长时以下高级特性和实践能帮助你更好地组织代码。5.1 生命周期钩子HooksRemult 允许你在实体保存、删除、验证等生命周期节点注入逻辑。这是实现审计日志、自动填充字段、复杂业务规则验证的绝佳位置。Entity(“tasks”, { … }) export class Task { // … 字段定义 // 在保存前自动设置 updatedAt 和 updatedBy Fields.updatedAt() updatedAt?: Date; Fields.string({ allowApiUpdate: false }) updatedBy?: string; // 生命周期钩子 BackendMethod({ allowed: false }) // 仅在后端运行 static async beforeSave(task: Task, remult: Remult) { if (task._.isNew()) { // 新建任务 task.createdBy remult.user?.name; } else { // 更新任务 task.updatedBy remult.user?.name; task.updatedAt new Date(); } // 业务规则已完成的任务必须有完成时间 if (task.completed !task.completedAt) { task.completedAt new Date(); } } } // 注册钩子 Task.$.beforeSave(Task.beforeSave);5.2 复杂查询与关系处理Remult 的查询构建器非常强大支持关联查询Relations。假设我们有一个User实体Task属于一个User。// shared/User.ts Entity(“users”) export class User { Fields.uuid() id “”; Fields.string() name “”; } // shared/Task.ts Entity(“tasks”) export class Task { Fields.uuid() id “”; Fields.string() title “”; // 定义关系字段 Field(() User) owner?: User; Fields.string() ownerId “”; // 外键字段 }在前端你可以通过include选项一次性加载关联的owner信息避免 N1 查询问题。const tasksWithOwner await taskRepo.find({ include: { owner: true } // 自动联表查询 }); console.log(tasksWithOwner[0].owner.name); // 直接访问5.3 性能优化分页、筛选与实时数据分页所有find查询都原生支持limit和page参数对于列表页面至关重要。const page1 await repo.find({ page: 1, limit: 50 });服务器端筛选与排序查询条件where和排序orderBy都会转化为数据库查询语句在服务器端执行保证效率。实时/订阅Remult 通过其liveQuery功能支持实时数据更新。这对于看板、协作编辑等场景非常有用。// 前端订阅任务列表的变化 useEffect(() { const subscription taskRepo.liveQuery({ where: { completed: false } }).subscribe(info { setTasks(info.items); // info.items 会在数据变化时自动更新 }); return () subscription.unsubscribe(); }, []);5.4 测试策略Remult 的架构非常有利于测试。单元测试业务逻辑你可以直接实例化实体仓库传入一个内存数据提供者在不启动服务器的情况下测试验证规则、钩子和控制器方法。集成测试 API可以使用像supertest这样的工具对你的 Next.js API 路由即 Remult 的 API 入口进行测试覆盖完整的请求-响应流程。前端组件测试可以利用 Remult 提供的测试工具或 Mock为使用useRemult或taskRepo的组件提供模拟数据。6. 常见问题与排查实录在实际使用中你可能会遇到一些典型问题。以下是我踩过的一些坑和解决方案。6.1 类型错误“Property ‘_’ does not exist on type…”这是 Remult 实体元数据访问的方式。你需要通过实体类的$静态属性或实例的_.来访问 Remult 添加的元数据和方法如isNew(),save()。确保你从remult包正确导入了实体类并且装饰器已应用。import { Task } from “../shared/Task”; // 正确导入装饰后的类 // import { Task } from “./some-other-place”; // 错误可能导入的是未装饰的类副本 const task new Task(); task._.isNew(); // 正确访问 Task.$.find(); // 静态访问6.2 API 返回 404 或 405检查实体注册确保你在remultNextApp或类似初始化函数的entities数组中注册了所有需要通过 API 访问的实体。检查 HTTP 方法Remult 默认只为允许的操作生成端点。例如如果allowApiUpdate是false那么PUT /api/tasks/123会返回 405 Method Not Allowed。检查你的实体和字段的allowApi…配置。检查路由配置在 Next.js App Router 中确保文件路径是app/api/[...remult]/route.ts。在 Pages Router 中则是pages/api/[...remult].ts。一个字符的错误都会导致路由不匹配。6.3 数据库连接或适配器问题createPrismaDataProvider is not a function确保你安装了正确版本的适配器包remult/adapters-prisma并且导入路径正确。不同 Remult 版本的适配器可能有差异。连接池耗尽Serverless 环境在 Vercel 等无服务器环境中每个请求可能创建新的数据库连接。务必按照 Prisma 或你所用 ORM 的官方文档配置连接池或重用客户端实例。通常在模块顶层缓存一个 Prisma Client 单例是安全的。6.4 权限allowApi…或apiPrefilter不生效权限检查发生在 Remult 的 API 处理层。如果直接在后端代码中通过remult.repo(Task).find()查询某些基于remult.user的权限如apiPrefilter可能仍然有效但allowApi…只对 HTTP API 请求有效。确保你理解上下文是在处理 API 请求remult上下文已注入用户信息还是在执行后台任务可能需要手动设置remult.user。6.5 处理文件上传等非 JSON 请求Remult 主要处理 JSON API。对于文件上传一个常见的模式是使用传统的 multipart 表单处理端点如Next.js的API Route接收文件将其保存到对象存储如 S3或服务器磁盘。将文件访问路径URL作为一个字符串字段通过 Remult 实体进行保存和关联。在实体中这个文件路径字段就是一个普通的字符串Remult 可以完美处理它的 CRUD 和权限。6.6 与现有后端或前端状态库集成如果你有一个庞大的现有后端Remult 可能不适合一次性全面接管。但你可以逐步采用前端先行在前端项目中使用 Remult 的客户端来管理状态和调用你现有的 API通过自定义fetch实现。这能立即获得类型安全的客户端体验。后端部分模块在新开发的模块或微服务中使用 Remult 作为全栈框架享受其完整能力。它不要求你重写一切。对于前端状态库如 Redux, ZustandRemult 的repo和liveQuery本身就是一个优秀的状态管理层。在很多场景下你不再需要它们。但如果已有复杂状态逻辑Remult 可以与之共存专注于数据获取和同步。7. 总结与个人体会经过几个项目的深度使用Remult 给我的最大感受是“收敛”。它收敛了前后端的类型定义收敛了 API 的样板代码收敛了权限和验证的分散逻辑。它迫使你用一种更清晰、更以领域模型为中心的方式来思考应用架构。初期需要一点学习成本来理解其“约定大于配置”的哲学但一旦适应开发效率的提升是线性的尤其是对于中后台、CRUD 密集型的管理系统。它并非银弹。对于极度追求 RESTful 纯净性、或已有复杂异构技术栈的项目引入 Remult 需要权衡。但对于全新的、尤其是中小型全栈 TypeScript 项目我几乎会毫不犹豫地推荐尝试。从快速原型到稳定生产它都能提供坚实的支撑。最后一个小技巧充分利用 Remult 的liveQuery和 React 集成可以轻松构建出响应极其灵敏的实时应用界面这往往是给用户带来“哇哦”体验的关键。