关于 safe-action-middleware
用于构建 next-safe-action 中间件链的参考技能,涵盖验证前后中间件、上下文传递、执行顺序及常见链式调用问题。
主要功能
- 01使用 .use() 链接中间件
- 02使用 .useValidated() 运行类型化中间件
- 03通过 next() 累积上下文
- 04指定中间件执行顺序
- 05处理 Schema 转换与错误
适用场景
- 01在 Action 执行前添加身份验证
- 02根据验证后的输入进行授权
- 03记录或审计解析后的输入
- 04在中间件间共享上下文
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ next-safe-action/ skills --skill safe-action-middleware安装后在 Claude Code 中输入/safe-action-middleware确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
auth-patterns.mdlogging-monitoring.mdstandalone-middleware.md详细介绍
next-safe-action 中间件
快速入门
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 },
});
});
中间件的工作方式
.use()会向调用链添加中间件——可以多次调用- 每次调用
.use()都会返回一个新的客户端实例(不可变调用链) - 中间件按从上到下的顺序执行(按添加顺序)
- 结果按从下到上的顺序流动(最深层的中间件/操作最先完成解析)
- 上下文通过
next({ ctx })累积——每一层的 ctx 都会与上一层的 ctx 深度合并
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() 中间件函数签名
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 */ } });
}
useValidated() — 验证后中间件
.useValidated() 会注册在输入验证之后运行的中间件,使其可以访问类型化的 parsedInput。默认使用 use()——只有当中间件逻辑依赖经过验证的输入时,才使用 useValidated()。
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 });
});
执行顺序
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 parsedInput
两种中间件调用链都遵循洋葱模型:next() 之前的代码按从上到下的顺序运行,next() 之后的代码按从下到上的顺序逐层返回。
use() 与 useValidated()
| 需求 | 方法 |
|---|---|
| 身份验证、日志记录、速率限制(不需要输入) | .use() |
在验证之前访问原始 clientInput | .use() |
| 根据经过验证的输入进行授权(例如,检查用户是否拥有资源) | .useValidated() |
| 记录或审计经过验证/转换的输入 | .useValidated() |
| 使用从解析后的输入派生的数据丰富上下文 | .useValidated() |
useValidated() 中间件函数签名
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 */ } });
}
链式调用规则
- 必须在
.useValidated()之前调用.inputSchema()或.bindArgsSchemas() - 不能在
.useValidated()之后调用.inputSchema()或.bindArgsSchemas() - 不能在
.useValidated()之后调用.use() - 可以链式调用多个
.useValidated()
模式转换
useValidated() 在 parsedInput 中看到的是转换后的值,而 clientInput 保留原始值:
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();
})
错误回调中的上下文
use()中间件设置的上下文在onError/onSettled回调中始终可用。useValidated()中间件设置的上下文是可选的(可能为undefined)——如果验证失败,useValidated()就不会运行,因此不会包含它添加的上下文。
相关文档
反模式
// 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: {} });
})
// 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" };
}
})
// 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();
});
评论
支持 Markdown