safe-action-middleware について
next-safe-action のミドルウェアチェーン構築に関するリファレンススキルです。検証前後の処理、コンテキスト伝播、実行順序、チェーンの注意点を説明します。
主な機能
- 01.use() によるミドルウェア連結
- 02.useValidated() による型付き処理
- 03next() を介したコンテキストの蓄積
- 04ミドルウェアの実行順序設定
- 05Schema 変換とエラーの処理
活用シーン
- 01アクション前の認証追加
- 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 は、前の値とディープマージされます
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
どちらのミドルウェアスタックもオンionモデルに従います。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 対応