Safe Action Middleware

next-safe-action/skills/skills/safe-action-middleware

作者 next-safe-actiona2605bd2e84321245cba8f5718c144a6e4a5fa47無授權條款收錄於 2026年10月9日更新於 2026年10月9日

Use when implementing middleware for next-safe-action -- authentication, authorization, logging, rate limiting, error interception, context extension, or creating standalone reusable middleware with createMiddleware() or createValidatedMiddleware(). Covers both use() (pre-validation) and useValidated() (post-validation) middleware.

AI 產生的概覽

指導實作 next-safe-action 中介軟體,涵蓋驗證、記錄、限流、錯誤處理與內容延伸。

功能
說明如何為 next-safe-action 函式庫建立中介軟體鏈,涵蓋 .use() 驗證前中介軟體與 .useValidated() 驗證後中介軟體。文件介紹函式簽章、執行順序、內容累積與深度合併、鏈式呼叫規則、結構描述轉換,以及錯誤回呼中內容的可用性。也列出反模式,例如忘記回傳 next() 或吞掉框架錯誤,並指向驗證模式、記錄與獨立中介軟體等支援文件。
適用情境
在為 next-safe-action 伺服器動作加入驗證、授權、記錄、限流、錯誤攔截或內容延伸時使用。也適用於在 .use() 與 .useValidated() 之間做選擇,或用 createMiddleware()、createValidatedMiddleware() 建立可重用的獨立中介軟體時。
執行需求
不需要指令碼或工具,代理只需技能指示。內容針對 next-safe-action 函式庫與 TypeScript/Next.js 專案,範例涉及工作階段輔助函式、資料庫與 Zod 結構描述。

next-safe-action Middleware

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 },  });});

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() Middleware Function Signature

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 */ } });}

useValidated() — Post-Validation Middleware

.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 });  });

Execution Order

1. use() middleware            — pre-validation, runs in order added2. Input validation            — schema parsing3. useValidated() middleware   — post-validation, runs in order added4. 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.

use() vs 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()

useValidated() Middleware Function Signature

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 */ } });}

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

Schema Transforms

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();  })

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.

Supporting Docs

  • Authentication & authorization patterns
  • Logging & monitoring middleware
  • Standalone reusable middleware with createMiddleware() and 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 compileconst 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();  });

來源與署名

來源:next-safe-action/skills位於skills/safe-action-middleware提交a2605bd

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架