safe-action-middleware

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

next-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

中间件工作原理

  • .use()
    adds middleware to the chain — you can call it multiple times
  • Each
    .use()
    returns a new client instance (immutable chain)
  • Middleware executes top-to-bottom (in the order added)
  • Results flow bottom-to-top (the deepest middleware/action resolves first)
  • Context is accumulated via
    next({ ctx })
    — each level's ctx is deep-merged with the previous
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()
    都会返回一个全新的客户端实例(不可变调用链)
  • 中间件从上到下执行(按添加顺序运行)
  • 执行结果从下到上传递(最深层的中间件/动作最先完成解析)
  • 上下文通过
    next({ ctx })
    累积 —— 每一层的ctx都会与上一层的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()
registers middleware that runs after input validation, giving access to typed
parsedInput
. Default to
use()
— only use
useValidated()
when middleware logic depends on validated input.
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()
用于注册在输入校验完成后执行的中间件,你可以在其中访问带类型的
parsedInput
。默认优先使用
use()
—— 只有当中间件逻辑需要依赖校验后的输入时,再使用
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 parsedInput
Both middleware stacks follow the onion model: code before
next()
runs top-to-bottom, code after
next()
unwinds bottom-to-top.
1. use() 中间件            —— 校验前执行,按添加顺序运行
2. 输入校验            —— schema解析
3. useValidated() 中间件   —— 校验后执行,按添加顺序运行
4. 服务端代码 (.action())     —— 接收最终的ctx和parsedInput
两类中间件栈都遵循洋葱模型:
next()
之前的代码从上到下执行,
next()
之后的代码从下到上回溯执行。

use() vs useValidated()

use() 与 useValidated() 对比

NeedMethod
Authentication, logging, rate limiting (no input needed)
.use()
Access to raw
clientInput
before validation
.use()
Authorization based on validated input (e.g., check user owns resource)
.useValidated()
Logging or auditing validated/transformed input
.useValidated()
Enriching context with data derived from parsed input
.useValidated()
需求方法
身份认证、日志记录、限流(不需要输入参数)
.use()
在校验前访问原始
clientInput
.use()
基于校验后的输入做权限校验(例如校验用户是否是资源所有者)
.useValidated()
对校验/转换后的输入做日志或审计
.useValidated()
基于解析后的输入扩展上下文数据
.useValidated()

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
    .inputSchema()
    or
    .bindArgsSchemas()
    before
    .useValidated()
  • Cannot call
    .inputSchema()
    or
    .bindArgsSchemas()
    after
    .useValidated()
  • Cannot call
    .use()
    after
    .useValidated()
  • Can chain multiple
    .useValidated()
    calls
  • 必须在调用
    .useValidated()
    之前调用
    .inputSchema()
    .bindArgsSchemas()
  • 不能在调用
    .useValidated()
    之后调用
    .inputSchema()
    .bindArgsSchemas()
  • 不能在调用
    .useValidated()
    之后调用
    .use()
  • 可以链式调用多个
    .useValidated()

Schema Transforms

Schema转换

useValidated()
sees the transformed value in
parsedInput
, while
clientInput
retains the original:
ts
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()
中访问的
parsedInput
转换后的值,而
clientInput
保留原始值:
ts
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
    use()
    middleware is always available in
    onError
    /
    onSettled
    callbacks.
  • Context set by
    useValidated()
    middleware is optional (may be
    undefined
    ) — if validation fails,
    useValidated()
    never runs, so its context additions are missing.
  • 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();
  });