ルートコントラクト
ルートコントラクトは、ルートを登録するときに第 2 引数に渡すオプションのオブジェクトです。そこに書いたスキーマで、アクションの前にリクエストが検証されます。同じ定義から、ルートヘルパーと API クライアントの型、OpenAPI ドキュメント、エージェントツールも作られます。パス、グループ、名前、ミドルウェア、モデルバインディングといったルートの登録そのものは、ルーティングで扱います。
ルートコントラクト
ルートコントラクトは、ルートを登録するときに第 2 引数に渡すオプションのオブジェクトです。そこに書いたスキーマで、アクションの前にリクエストが検証されます。同じ定義から、ルートヘルパーと API クライアントの型、OpenAPI ドキュメント、エージェントツールも作られます。パス、グループ、名前、ミドルウェア、モデルバインディングといったルートの登録そのものは、ルーティングで扱います。
コントラクトの宣言
オプションのオブジェクトは、パスとハンドラーのあいだに置きます。スキーマは zod 4 の API(import { z } from 'zod')で書いてください。zod v3 の API で書いたスキーマは、構造を読み取るツールが警告を出して受け付けません(バリデーション を参照)。
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1),
body: z.string().min(1),
})
const PostIdParams = z.object({
id: z.coerce.number().int().positive(),
})
router.post('/posts', {
body: CreatePostSchema,
name: 'posts.store',
}, [PostsController, 'store'])
router.get('/posts/:id', {
params: PostIdParams,
name: 'posts.show',
}, [PostsController, 'show'])
使えるコントラクトのフィールドは次のとおりです。
| フィールド | 用途 |
|---|---|
name |
URL 生成・コード生成用のルート名 |
params |
パスパラメータの Zod スキーマ |
query |
クエリパラメータの Zod スキーマ |
body |
リクエストボディの Zod スキーマ |
output |
レスポンスボディの Zod スキーマ。検証するのは 2xx の応答だけなので、エラーやリダイレクトはアプリが書いたまま返る |
resource |
Resource クラスによるレスポンスのヒント(スキーマを書かずに API クライアントを型付けする) |
bind |
ルートモデルバインディングの対応表。{ id: Post }(主キー)または { slug: [Post, 'slug'] }(別のカラム) |
middlewares |
ミドルウェアハンドラーの配列 |
note
同じクエリキーが繰り返された場合、query スキーマには配列として渡されます(?tag=a&tag=b → { tag: ['a', 'b'] })。1 回しか出てこないキーは文字列のままです。詳しくは配列形式のクエリパラメータを参照してください。
検証済み入力の読み取り
params、query、body のスキーマは、インラインハンドラーでもコントローラーのアクションでも、ハンドラーより先に検査されます。どれかに違反したリクエストには 422 を返し、アクションは実行しません。Inertia のリクエストでは、エラーが次のリクエストまで保持されてフォームに戻り、キーは validateBody() と同じフィールドパスになります。
アクションでは、スキーマがパースした値を this.validated() で読みます。引数には、そのアクションのルート名を渡します。
import type { UserRecord } from '@/app/Models/User'
export default class PostsController extends Controller {
async store() {
const { body } = this.validated('posts.store')
const user = await this.auth.userOrFail<UserRecord>()
const post = await Post.create(body, { set: { authorId: user.id } })
return this.redirect(`/posts/${post.id}`)
}
}
- 値は型変換、デフォルト値、変換処理を適用した後の形で届くので、
z.coerce.number()のパラメータはnumberになります。スキーマを宣言していないセグメントはundefinedです。 .transform()で終わるフィールドはunknown型になります。関数が返す値の型を codegen が読めないためです。型を付けたいときは後ろにスキーマをつなぎます。z.string().transform(Number).pipe(z.number())ならnumberになります。guren codegenでルートの型を生成しておくと、ルート名がコンパイル時に検査され、戻り値もコントラクトから型付けされます。処理中のルートと違う名前を渡すと、実行時に例外になります。- PUT と PATCH のように 1 つのアクションを複数のルートに割り当てる場合は、
this.validated(['posts.update', 'posts.patch'])のようにすべての名前を渡します。 - コントラクトはアクションより先に実行されるので、不正なボディには、アクションの中の検査(
this.auth.userOrFail()など)より先に 422 が返ります。未認証のリクエストに 401 を先に返したい場合は、その検査をルートのミドルウェアに置いてください。
コントラクトを宣言していないルートは、これまでどおり validateBody()、validateQuery()、validateParams() で検証します(バリデーションを参照)。
Resource レスポンスヒント
API リソースで応答するルートには、すでにレスポンスの型があります。codegen が Resource クラスから .guren/data.gen.ts に抽出する型です。そのようなルートに output スキーマを書くと、同じ形を Zod でもう一度定義することになり、2 つの定義が少しずつ食い違っていきます。output を書く代わりに、Resource そのものを宣言してください。
import { PostResource } from '@/app/Http/Resources/PostResource'
router.get('/api/posts', {
name: 'posts.index',
resource: { data: [PostResource] },
}, [PostsController, 'index'])
ヒントは、コントローラーが組み立てる JSON と同じ形で書きます。単一のリソースはクラスそのもの(resource: PostResource)、コレクションは要素が 1 つの配列(resource: [PostResource])、エンベロープはプレーンオブジェクトで表します。たとえば { data: [PostResource] } は this.json({ data: PostResource.collection(posts) }) に対応します。入れ子は何段でも書けます。
guren codegen を実行すると、各クラスがプロジェクトルートと各 modules/<name>/ の app/Http/Resources から探され、組み立てた形(この例では { data: Data.Post[] })で、生成された API クライアントの json() に型が付きます。
output スキーマ |
resource ヒント |
|
|---|---|---|
| リクエスト時の検査 | あり(2xx の応答) | なし |
| API クライアントの型付け | あり | あり |
| OpenAPI ドキュメントへの出力 | あり | なし(openapi:generate が警告する) |
| 両方を指定したとき | 優先される | 無視される |
見つからない Resource クラスをヒントに書くと、codegen が警告を出し、レスポンスは型なしのままになります。
note
ヒントの末端は、すべて Resource クラスでなければなりません。Resource と通常の型付きオブジェクトが混ざったエンベロープ(たとえばページネーションのレスポンスにある meta と links)は、今のところ表現できません。そのようなルートには output スキーマを使ってください。
エージェントツール
名前付きのルートに agent メタデータを宣言すると、そのルートが MCP ツールとして AI エージェントに公開されます。ツールの入力スキーマ、出力スキーマ、認可はすべて上のコントラクトから導出されるので、同じ内容を書き直す必要はありません。
// メソッドチェーン
router
.post('/posts', { body: CreatePostSchema, output: PostResponseSchema }, [PostsController, 'store'])
.name('posts.store')
.agent({ description: 'Create a blog post as the authenticated user.' })
// ルートコントラクトのキーとして
router.post('/posts', {
name: 'posts.store',
body: CreatePostSchema,
agent: { description: 'Create a blog post as the authenticated user.' },
}, [PostsController, 'store'])
resource() でも、アクションごとに同じメタデータを渡せます。列挙しなかったアクションは公開されません。
router.resource('/posts', PostsController, {
agent: {
index: { description: 'List posts.' },
show: { description: 'Fetch one post by id.' },
},
})
公開するかどうかはルートごとに選ぶオプトイン方式です。ツール名にはルート名がそのまま使われるので、.name() のないルートはツールにできません。ルートオプションの agent と .agent() のチェーンを両方書くと、登録時に例外を投げます。宣言は 1 か所だけにしてください。
エージェントから何が見えるかは、bunx guren tool:list で確認できます。メタデータの各フィールド、入出力の導出ルール、MCP エンドポイントについてはエージェントインターフェースガイドを、トークンのスコープ、承認、監査ログについてはエージェントツールの権限と監査を参照してください。