safe-action-middleware
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesenext-safe-action Middleware
next-safe-action 中间件
Quick Start
快速开始
ts
import { createSafeActionClient } from "next-safe-action";
const actionClient = createSafeActionClient();
// Add middleware with .use()
const authClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session?.user) {
throw new Error("Unauthorized");
}
// Pass context to the next middleware/action via next({ ctx })
return next({
ctx: { userId: session.user.id },
});
});ts
import { createSafeActionClient } from "next-safe-action";
const actionClient = createSafeActionClient();
// 通过.use()添加中间件
const authClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session?.user) {
throw new Error("Unauthorized");
}
// 通过next({ ctx })将上下文传递给下一个中间件/动作
return next({
ctx: { userId: session.user.id },
});
});How Middleware Works
中间件工作原理
- adds middleware to the chain — you can call it multiple times
.use() - Each returns a new client instance (immutable chain)
.use() - Middleware executes top-to-bottom (in the order added)
- Results flow bottom-to-top (the deepest middleware/action resolves first)
- Context is accumulated via — each level's ctx is deep-merged with the previous
next({ ctx })
ts
const client = createSafeActionClient()
.use(async ({ next }) => {
console.log("1: before"); // Runs 1st
const result = await next({ ctx: { a: 1 } });
console.log("1: after"); // Runs 4th
return result;
})
.use(async ({ next, ctx }) => {
console.log("2: before", ctx.a); // Runs 2nd, ctx.a = 1
const result = await next({ ctx: { b: 2 } });
console.log("2: after"); // Runs 3rd
return result;
});
// In .action(): ctx = { a: 1, b: 2 }- 会向调用链中添加中间件 —— 你可以多次调用该方法
.use() - 每次调用都会返回一个全新的客户端实例(不可变调用链)
.use() - 中间件从上到下执行(按添加顺序运行)
- 执行结果从下到上传递(最深层的中间件/动作最先完成解析)
- 上下文通过累积 —— 每一层的ctx都会与上一层的ctx进行深度合并
next({ ctx })
ts
const client = createSafeActionClient()
.use(async ({ next }) => {
console.log("1: before"); // 第1个执行
const result = await next({ ctx: { a: 1 } });
console.log("1: after"); // 第4个执行
return result;
})
.use(async ({ next, ctx }) => {
console.log("2: before", ctx.a); // 第2个执行,ctx.a = 1
const result = await next({ ctx: { b: 2 } });
console.log("2: after"); // 第3个执行
return result;
});
// 在.action()中: ctx = { a: 1, b: 2 }use() Middleware Function Signature
use() 中间件函数签名
ts
async ({
clientInput, // Raw input from the client (unknown)
bindArgsClientInputs, // Raw bind args array
ctx, // Accumulated context from previous middleware
metadata, // Metadata set via .metadata()
next, // Call to proceed to next middleware/action
}) => {
// Optionally extend context
return next({ ctx: { /* new context properties */ } });
}ts
async ({
clientInput, // 来自客户端的原始输入(类型为unknown)
bindArgsClientInputs, // 原始绑定参数数组
ctx, // 从前面中间件累积的上下文
metadata, // 通过.metadata()设置的元数据
next, // 调用该方法进入下一个中间件/动作
}) => {
// 可选择扩展上下文
return next({ ctx: { /* 新的上下文属性 */ } });
}useValidated() — Post-Validation Middleware
useValidated() —— 校验后执行的中间件
.useValidated()parsedInputuse()useValidated()ts
const action = authClient
.inputSchema(z.object({ postId: z.string().uuid(), title: z.string() }))
.useValidated(async ({ parsedInput, ctx, next }) => {
// parsedInput is typed: { postId: string; title: string }
const post = await db.post.findById(parsedInput.postId);
if (!post || post.authorId !== ctx.userId) {
throw new Error("Not authorized");
}
return next({ ctx: { post } });
})
.action(async ({ parsedInput, ctx }) => {
// ctx.post is available and typed
await db.post.update(ctx.post.id, { title: parsedInput.title });
});.useValidated()parsedInputuse()useValidated()ts
const action = authClient
.inputSchema(z.object({ postId: z.string().uuid(), title: z.string() }))
.useValidated(async ({ parsedInput, ctx, next }) => {
// parsedInput带类型: { postId: string; title: string }
const post = await db.post.findById(parsedInput.postId);
if (!post || post.authorId !== ctx.userId) {
throw new Error("Not authorized");
}
return next({ ctx: { post } });
})
.action(async ({ parsedInput, ctx }) => {
// ctx.post可用且带类型
await db.post.update(ctx.post.id, { title: parsedInput.title });
});Execution Order
执行顺序
1. use() middleware — pre-validation, runs in order added
2. Input validation — schema parsing
3. useValidated() middleware — post-validation, runs in order added
4. Server code (.action()) — receives final ctx and parsedInputBoth middleware stacks follow the onion model: code before runs top-to-bottom, code after unwinds bottom-to-top.
next()next()1. use() 中间件 —— 校验前执行,按添加顺序运行
2. 输入校验 —— schema解析
3. useValidated() 中间件 —— 校验后执行,按添加顺序运行
4. 服务端代码 (.action()) —— 接收最终的ctx和parsedInput两类中间件栈都遵循洋葱模型:之前的代码从上到下执行,之后的代码从下到上回溯执行。
next()next()use() vs useValidated()
use() 与 useValidated() 对比
| Need | Method |
|---|---|
| Authentication, logging, rate limiting (no input needed) | |
Access to raw | |
| Authorization based on validated input (e.g., check user owns resource) | |
| Logging or auditing validated/transformed input | |
| Enriching context with data derived from parsed input | |
| 需求 | 方法 |
|---|---|
| 身份认证、日志记录、限流(不需要输入参数) | |
在校验前访问原始 | |
| 基于校验后的输入做权限校验(例如校验用户是否是资源所有者) | |
| 对校验/转换后的输入做日志或审计 | |
| 基于解析后的输入扩展上下文数据 | |
useValidated() Middleware Function Signature
useValidated() 中间件函数签名
ts
async ({
parsedInput, // Validated, typed input (from inputSchema)
clientInput, // Raw input from the client
bindArgsParsedInputs, // Validated bind args tuple
bindArgsClientInputs, // Raw bind args array
ctx, // Accumulated context from all previous middleware
metadata, // Metadata set via .metadata()
next, // Call to proceed to next middleware/action
}) => {
return next({ ctx: { /* new context properties */ } });
}ts
async ({
parsedInput, // 校验后的带类型输入(来自inputSchema)
clientInput, // 来自客户端的原始输入
bindArgsParsedInputs, // 校验后的绑定参数元组
bindArgsClientInputs, // 原始绑定参数数组
ctx, // 从所有前面中间件累积的上下文
metadata, // 通过.metadata()设置的元数据
next, // 调用该方法进入下一个中间件/动作
}) => {
return next({ ctx: { /* 新的上下文属性 */ } });
}Chaining Rules
链式调用规则
- Must call or
.inputSchema()before.bindArgsSchemas().useValidated() - Cannot call or
.inputSchema()after.bindArgsSchemas().useValidated() - Cannot call after
.use().useValidated() - Can chain multiple calls
.useValidated()
- 必须在调用之前调用
.useValidated()或.inputSchema().bindArgsSchemas() - 不能在调用之后调用
.useValidated()或.inputSchema().bindArgsSchemas() - 不能在调用之后调用
.useValidated().use() - 可以链式调用多个
.useValidated()
Schema Transforms
Schema转换
useValidated()parsedInputclientInputts
authClient
.inputSchema(z.string().transform((s) => s.toUpperCase()))
.useValidated(async ({ clientInput, parsedInput, next }) => {
console.log(clientInput); // "hello" (original)
console.log(parsedInput); // "HELLO" (transformed)
return next();
})useValidated()parsedInputclientInputts
authClient
.inputSchema(z.string().transform((s) => s.toUpperCase()))
.useValidated(async ({ clientInput, parsedInput, next }) => {
console.log(clientInput); // "hello"(原始值)
console.log(parsedInput); // "HELLO"(转换后的值)
return next();
})Context in Error Callbacks
错误回调中的上下文
- Context set by middleware is always available in
use()/onErrorcallbacks.onSettled - Context set by middleware is optional (may be
useValidated()) — if validation fails,undefinednever runs, so its context additions are missing.useValidated()
- 中间件设置的上下文始终可以在
use()/onError回调中获取。onSettled - 中间件设置的上下文是可选的(可能为
useValidated())—— 如果校验失败,undefined不会执行,因此它添加的上下文也不存在。useValidated()
Supporting Docs
参考文档
- Authentication & authorization patterns
- Logging & monitoring middleware
- Standalone reusable middleware with createMiddleware() and createValidatedMiddleware()
- 身份认证与权限校验模式
- 日志与监控中间件
- 使用createMiddleware()和createValidatedMiddleware()创建独立可复用中间件
Anti-Patterns
反模式
ts
// BAD: Forgetting to return next() — action will hang
.use(async ({ next }) => {
await doSomething();
next({ ctx: {} }); // Missing return!
})
// GOOD: Always return the result of next()
.use(async ({ next }) => {
await doSomething();
return next({ ctx: {} });
})ts
// BAD: Catching all errors (swallows framework errors like redirect/notFound)
.use(async ({ next }) => {
try {
return await next({ ctx: {} });
} catch (error) {
return { serverError: "Something went wrong" }; // Swallows redirect!
}
})
// GOOD: Re-throw framework errors
.use(async ({ next }) => {
try {
return await next({ ctx: {} });
} catch (error) {
if (error instanceof Error && "digest" in error) {
throw error; // Let Next.js handle redirects, notFound, etc.
}
// Handle other errors
console.error(error);
return { serverError: "Something went wrong" };
}
})ts
// BAD: useValidated() without an input schema — won't compile
const client = actionClient.useValidated(async ({ parsedInput, next }) => {
return next();
});
// GOOD: Always define inputSchema or bindArgsSchemas before useValidated()
const client = actionClient
.inputSchema(z.object({ id: z.string() }))
.useValidated(async ({ parsedInput, next }) => {
console.log(parsedInput.id); // Typed!
return next();
});ts
// 错误写法:忘记返回next() —— 动作会卡住
.use(async ({ next }) => {
await doSomething();
next({ ctx: {} }); // 缺少return!
})
// 正确写法:始终返回next()的执行结果
.use(async ({ next }) => {
await doSomething();
return next({ ctx: {} });
})ts
// 错误写法:捕获所有错误(会吞掉框架内置错误比如redirect/notFound)
.use(async ({ next }) => {
try {
return await next({ ctx: {} });
} catch (error) {
return { serverError: "Something went wrong" }; // 吞掉了redirect!
}
})
// 正确写法:重新抛出框架内置错误
.use(async ({ next }) => {
try {
return await next({ ctx: {} });
} catch (error) {
if (error instanceof Error && "digest" in error) {
throw error; // 让Next.js处理重定向、notFound等逻辑
}
// 处理其他错误
console.error(error);
return { serverError: "Something went wrong" };
}
})ts
// 错误写法:没有定义输入schema就使用useValidated() —— 无法通过编译
const client = actionClient.useValidated(async ({ parsedInput, next }) => {
return next();
});
// 正确写法:在调用useValidated()之前始终定义inputSchema或bindArgsSchemas
const client = actionClient
.inputSchema(z.object({ id: z.string() }))
.useValidated(async ({ parsedInput, next }) => {
console.log(parsedInput.id); // 带类型!
return next();
});