# Frourio Framework

> frourio (Fastify + aspida + zod ベースのTypeScriptフルスタックフレームワーク) の使い方。 ファイルベースルーティング規約、defineController/defineValidators/defineHooks パターン、 動的セグメント、自動生成ファイル ($server.ts, $relay.ts, $api.ts)、aspida型連携、 hooks 経由の JWT decode を controller で取得するパターン、 frourio-framework-prisma-generators による Prisma Model クラス生成と toDto 返却ルール、 フロントエンドでの useFrourioSWR (useEffect 回避) によるデータ取得規約、 新規ルート追加手順を網羅。最新テンプレートは frourio 本体を内蔵 (@frouvel/frourio)。 Triggers: "frourio", "ルート追加", "新しいAPI", "endpoint 追加", "controller 作る", "$server.ts", "$relay.ts", "aspida", "DefineMethods", "defineController", "frourio-framework-prisma-generators", "ModelDto", "toDto", "useFrourioSWR", "useAspidaSWR", "@frouvel/frourio".

- Skill: `interfacex-co-jp/frourio-framework` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add interfacex-co-jp/frourio-framework`
- Raw SKILL.md: https://api.skillmd.com/api/skills/interfacex-co-jp/frourio-framework/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: interfacex-co-jp (https://skillmd.com/u/interfacex-co-jp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/interfacex-co-jp/frourio-framework

---


# frourio Framework

frourio = Fastify + aspida + zod ベース TypeScript フルスタックフレームワーク。
ファイルベースルーティング (Next.js風)。`frourio` CLI で `$server.ts` / `$relay.ts` 自動生成。

最新の `frourio-framework` テンプレートは frourio 本体を内蔵 (`backend-api/@frouvel/frourio/`、CLI + src 同梱) → 外部 `frourio` パッケージへの依存なし。テンプレート: <https://github.com/InterfaceX-co-jp/frourio-framework-template>。

## 1. ディレクトリ規約

ルート単位 = `api/<path>/` ディレクトリ。各ディレクトリに以下ファイル配置。

| ファイル | 役割 | 必須 | 自動生成 |
|---|---|---|---|
| `index.ts` | aspida `DefineMethods` でHTTPメソッド型定義 | ✅ | ❌ |
| `controller.ts` | `defineController` で handler 実装 | ✅ | ❌ |
| `validators.ts` | `defineValidators` で zod 検証 | 動的セグメント時必須 | ❌ |
| `hooks.ts` | `defineHooks` で Fastify hooks (auth等) | 任意 | ❌ |
| `$relay.ts` | `defineController/defineHooks/defineValidators` 関数 export | ✅ | ✅ frourio生成 |

ルート全集計: プロジェクトルートの `$server.ts` (frourio生成、編集禁止)。
aspida クライアント型: `api/$api.ts` (生成)。

## 2. ルートパスマッピング

ディレクトリ構造 = URL パス。

- `api/users/` → `/users`
- `api/users/_id@string/` → `/users/:id` (params.id: string)
- `api/posts/_postId@number/comments/` → `/posts/:postId/comments` (params.postId: number)

動的セグメント: `_<name>@<type>` ディレクトリ名。型は `string` / `number`。

## 3. index.ts — aspida 型定義

```ts
import type { DefineMethods } from 'aspida';

export type Methods = DefineMethods<{
  get: {
    query: {
      page: number;
      limit: number;
      search?: string;
    };
    resBody: { data: UserDto[]; total: number };
  };
  post: {
    reqBody: { email: string; password: string };
    resBody: { token: string };
  };
}>;
```

定義可能キー: `query` / `reqBody` / `reqHeaders` / `reqFormat` / `resBody` / `resHeaders` / `status`。

## 4. controller.ts — defineController

```ts
import { defineController } from './$relay';

export default defineController(() => ({
  get: ({ query }) => ({
    status: 200,
    body: { data: [...], total: 0 },
  }),
  post: async ({ body }) => ({
    status: 200,
    body: { token: '...' },
  }),
}));
```

handler 引数: `{ query, params, body, headers }` (index.ts で定義したものだけ存在)。
handler 戻り値: `{ status, body, headers }` シェイプ (`status` は HTTP コード、`body` は resBody 型)。
非同期可: handler を `async` または Promise 返却に。

DI overload (velona `depend` 経由):

```ts
export default defineController({ userService }, ({ userService }, fastify) => ({
  get: async ({ params }) => ({ status: 200, body: await userService.find(params.id) }),
}));
```

### 4.1 hooks 経由で注入した request プロパティを controller で取得 (JWT 等)

`hooks.ts` で JWT decode 等を行い request オブジェクトに property 注入 → controller の handler 引数からそのまま取得可能。handler 第一引数は Fastify の `FastifyRequest` と同じ参照 → hooks で追加した値がそのまま見える。

**1. Fastify request 型拡張** (型補完用、例: `@fastify/jwt`):

```ts
// types/fastify.d.ts
import 'fastify';
type JwtPayload = { id: string; email: string; scope: string[] };

declare module 'fastify' {
  interface FastifyRequest {
    user: JwtPayload;
  }
}
```

**2. hooks で decode・注入**:

```ts
// hooks.ts
import { defineHooks } from './$relay';

export default defineHooks(() => ({
  onRequest: async (req, reply) => {
    try {
      req.user = await req.jwtVerify();   // @fastify/jwt が verify + decode
    } catch (err) {
      reply.code(401).send({ error: 'unauthorized' });
    }
  },
}));
```

**3. controller で取得** (handler 引数を destructure or 直接参照):

```ts
import { defineController } from './$relay';

export default defineController(() => ({
  // 引数全体を受けて user 参照
  get: (req) => ({ status: 200, body: { id: req.user.id } }),

  // destructure (body, query, params 等と並列で user 取り出せる)
  post: ({ body, user }) => ({
    status: 200,
    body: { ownerId: user.id, ...body },
  }),
}));
```

階層継承: 親ディレクトリ `hooks.ts` で `req.user` を注入すれば、子孫ルート全 controller で取得可能 (ルート単位で `hooks.ts` を再宣言する必要はない)。

## 5. validators.ts — defineValidators (動的ルート必須)

動的セグメント `_id@string` 等含むディレクトリは zod 検証必須。

```ts
import { z } from 'zod';
import { defineValidators } from './$relay';

export default defineValidators(() => ({
  params: z.object({ id: z.string() }),
  // query, body も同様に定義可能
}));
```

`$relay.ts` の `defineValidators` 型は params の名前/型に対応 (frourio が動的に生成)。

## 6. hooks.ts — defineHooks (Fastify hooks)

```ts
import { defineHooks } from './$relay';
import type { FastifyRequest, FastifyReply } from 'fastify';

const authMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
  // JWT 検証等
};

export default defineHooks(() => ({
  onRequest: [authMiddleware],
  // preParsing, preValidation, preHandler 利用可
}));
```

階層継承: 親ディレクトリ `hooks.ts` は子孫ルート全てに適用される。

### 6.1 共通 hooks ロジックは `middleware/` に切り出し (重複禁止)

複数ルートで同じ認証 / rate limit / logging 等を使うときは、ロジックを `hooks.ts` 内にインラインで書かず、`backend-api/middleware/<name>.ts` に切り出して各 `hooks.ts` から import する。`hooks.ts` は薄ラッパに留める。

**❌ アンチパターン** — 兄弟 `hooks.ts` に同じロジックをコピペ:

```ts
// api/admin/clients/.../tiktok/hooks.ts と api/admin/report/hooks.ts に同一の認証コード 60 行...
```

**✅ 推奨パターン** — middleware 切り出し + 階層継承活用:

```ts
// middleware/authAdminMiddleware.ts — 1箇所に集約
import type { FastifyReply, FastifyRequest } from 'fastify';

export const authAdminMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
  // token 抽出 → JWT verify → scope check
  const payload = await req.server.jwt.verify(token);
  if (!hasAdminScope(payload)) {
    reply.code(403).send({ error: 'forbidden' });
    return reply;
  }
};
```

```ts
// api/admin/clients/hooks.ts — 子孫 (_id@string/, tiktok/* 等) 全部に継承される
import { defineHooks } from './$relay';
import { authAdminMiddleware } from '$/middleware/authAdminMiddleware';

export default defineHooks(() => ({
  onRequest: authAdminMiddleware,
}));
```

ポイント:

- middleware は `(req: FastifyRequest, reply: FastifyReply) => Promise<void>` のシンプル形式に統一 (defineHooks のクロージャ引数 fastify は不要)
- Fastify インスタンスが必要なら `req.server` で取得 (`req.server.jwt.verify(token)` 等)
- 認証必須範囲の **共通祖先ディレクトリ** に `hooks.ts` を 1つだけ置けば、子孫全ルートに適用される — 子孫ごとに個別 `hooks.ts` を作る必要なし
- 認証範囲が異なる兄弟ディレクトリ (例: `auth/login` は token 発行 endpoint なので認証掛けたくない) がある場合は、共通祖先より下に分けて配置 (frourio hooks は階層継承で **重ねがけ**、override 不可)

## 7. $relay.ts — 自動生成 (編集禁止)

frourio が各ルートディレクトリに生成。以下を export:
- `defineController` (DI付き overload あり)
- `defineHooks`
- `defineValidators` (動的ルート時のみ、params 型と整合)
- `multipartFileValidator()` (zodで MultipartFile 検証)

## 8. 自動生成コマンド

frourio CLI 直接実行:

```bash
frourio --watch    # 外部 frourio パッケージ使用時
frourio            # 一回のみ
```

最新 `frourio-framework-template` (frourio 内蔵版) では `@frouvel/frourio/cli.ts` を tsx で直接呼ぶ:

```bash
tsx @frouvel/frourio/cli.ts --watch    # dev (= npm run dev:frourio)
tsx @frouvel/frourio/cli.ts --build    # build (= npm run build)
```

テンプレート標準 npm scripts:
- `dev:frourio` — `tsx @frouvel/frourio/cli.ts --watch` (ウォッチ生成)
- `generate` — `npm run cli -- generate` (kaname CLI が aspida + frourio + prisma + config + openapi を統合実行)
- `generate:frourio` — `npm run cli -- generate:frourio`
- `generate:db` — `npx prisma generate --schema=database/prisma/schema.prisma`

`$server.ts`, `$relay.ts`, `$api.ts` 編集禁止 → 次回生成で消える。

## 9. 起動フロー

```ts
import Fastify from 'fastify';
import server from './$server';

const fastify = Fastify();
server(fastify, { basePath: '/api' });
fastify.listen({ port: 3000 });
```

`$server.ts` の default export `(fastify, options) => void` で全ルート登録。
`options.basePath` で URL prefix、`options.multipart` で `@fastify/multipart` 設定。

## 10. 新規ルート追加手順

1. `api/<path>/index.ts` 作成 — `Methods` 型 (DefineMethods)
2. `api/<path>/controller.ts` 作成 — `defineController` で handler
3. 動的セグメント時: `_<name>@<type>/` ディレクトリ + `validators.ts`
4. auth 必要時: `hooks.ts` で `defineHooks({ onRequest: [authMiddleware] })`
5. `frourio --watch` 実行中なら自動再生成、停止時は `frourio` を一度実行
6. tsc で `$server.ts` の import 整合性確認

## 11. フロントエンド: useFrourioSWR

frontend からの API 呼び出しは **`useFrourioSWR` を最優先**。`useAspidaSWR` は legacy fallback。**`useEffect` でのデータ取得は避ける**。

`useFrourioSWR` 実体: `backend-api/@frouvel/kaname/http/client/browser/useFrourioSWR.ts` (フロント側で import 利用)。`useAspidaSWR` 互換 API + `useAspidaSWR` で解決できなかった **enable / 条件付き fetch** に対応 (継続メンテ)。

### 11.1 基本

```ts
import { useFrourioSWR } from '<path>/useFrourioSWR';
import { adminApiClient } from '<aspida client>';

// シンプル GET — キャッシュキー = $path() 自動生成
const { data, error, isLoading } = useFrourioSWR(adminApiClient.admin.users);
```

戻り値型: aspida endpoint の `$get()` 戻り値から自動 infer → `*ModelDto` までそのまま型補完。

### 11.2 query 付き

```ts
const { data } = useFrourioSWR(adminApiClient.hq.projects, {
  query: { status: 'IN_PROGRESS' },
});
// キャッシュキーに query 含まれる → query 変更で自動再フェッチ
```

### 11.3 動的セグメント

aspida client のメソッド呼び出しで params 注入:

```ts
const { data } = useFrourioSWR(adminApiClient.admin.tenants._tenantId(tenantId));
```

### 11.4 条件付き fetch (enable 相当)

`null` / `undefined` / `false` を endpoint に渡せば SWR が無効化 → `useEffect` + state 不要。

```ts
// id 確定時のみ fetch
const { data } = useFrourioSWR(
  id ? adminApiClient.admin.tenants._tenantId(id) : null,
);

// 認証済みのときだけ fetch
const { data } = useFrourioSWR(isAuthenticated && adminApiClient.admin.me);
```

### 11.5 SWR config

```ts
// 第2引数に config (query 不要時)
useFrourioSWR(adminApiClient.admin.users, { refreshInterval: 5000 });

// useAspidaSWR 互換: 第2引数 undefined + 第3引数に config
useFrourioSWR(adminApiClient.admin.users, undefined, { refreshInterval: 5000 });

// query + config
useFrourioSWR(
  adminApiClient.hq.projects,
  { query: { status: 'IN_PROGRESS' } },
  { refreshInterval: 5000 },
);
```

### 11.6 useEffect を避ける指針

- **データ取得**: `useFrourioSWR` (条件付き含む) → `useEffect` + `fetch` 禁止
- **依存値で再取得**: `useFrourioSWR` の query / params 経由 (キャッシュキー変化で自動再取得)
- **mutation 後の再取得**: `mutate()` (SWR 標準) 経由
- `useEffect` は副作用専用 (subscription、外部 system 同期等)。データ取得には使わない

### 11.7 useAspidaSWR からの移行

```ts
// before (useAspidaSWR)
const { data } = useAspidaSWR(adminApiClient.admin.users);
const { data } = useAspidaSWR(adminApiClient.hq.projects, { query: {...} });

// after (useFrourioSWR — シグネチャ互換)
const { data } = useFrourioSWR(adminApiClient.admin.users);
const { data } = useFrourioSWR(adminApiClient.hq.projects, { query: {...} });
```

差分: 条件付き fetch (`null`/`undefined`/`false` 渡し) が型安全に書ける + 継続メンテ。

## 12. Prisma Model 生成 (frourio-framework-prisma-generators)

公式リポジトリ: <https://github.com/InterfaceX-co-jp/frourio-framework-prisma-generators>

各 Prisma model からイミュータブルな Domain Model クラスと DTO 型を自動生成 →
API 応答は **必ず生成 Model クラスの `toDto()` 経由で返却**。Prisma 生型を直接返さない。

### 11.1 要件 / インストール

- `prisma` と `@prisma/client` は **v7.2.0 以上、かつ同一バージョン**
- インストール: `npm install -D frourio-framework-prisma-generators`

### 11.2 生成器設定

`schema.prisma` にジェネレーターブロック追加:

```prisma
generator frourio_framework_prisma_model_generator {
    provider           = "frourio-framework-prisma-model-generator"
    output             = "__generated__/models"
    additionalTypePath = "./@additionalType/index"  // @json アノテーション使用時のみ必須
}
```

オプション:
- `provider`: 固定値
- `output`: 生成先 (schema からの相対パス)
- `additionalTypePath`: `@json` で参照する独自型の import 先

### 11.3 生成構造 — 1モデルあたり

例 schema:

```prisma
model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  title     String
  content   String?
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```

生成される 5 種類:

**A. `{Model}ModelDto`** — `toDto()` 戻り値型。`DateTime → string` (ISO 8601)。
**FK は relation field 存在時に自動除外** (例: `authorId` は relation `author` があるため除外)。

```ts
export type PostModelDto = {
  id: number;
  createdAt: string;
  title: string;
  content?: string | null;
  author?: UserWithIncludes | null;
  // authorId は出力されない
};
```

**B. `{Model}ModelConstructorArgs`** — `Date` のまま保持。

**C. `{Model}ModelFromPrismaValueArgs`** — `self: Prisma{Model}` + 関連を個別キーで。

```ts
export type PostModelFromPrismaValueArgs = {
  self: PrismaPost;
  author?: UserWithIncludes;
};
```

**D. `{Model}Model` class** — `private readonly` + getter + `static fromPrismaValue` + `toDto` + `static builder()`。

**E. `{Related}WithIncludes`** — `Prisma.{Model}GetPayload<typeof include>` ベースの関連込み型。

### 11.4 型変換ルール

| Prisma 型 | Constructor / Getter | DTO (`toDto()`) | 変換 |
|---|---|---|---|
| `String` / `Int` / `Float` / `Boolean` | 同名 | 同名 | なし |
| `DateTime` | `Date` | `string` | `.toISOString()` |
| `Decimal` | `number` | `number` | `fromPrismaValue` 内で `.toNumber()` |
| `BigInt` | `bigint` | `string` | `.toString()` |
| `Bytes` | `Buffer` | `string` | `Buffer.from().toString('base64')` |
| `Json` | `Prisma.JsonValue` | `Prisma.JsonValue` | なし (`@json` で上書き可) |
| Enum | `Prisma{EnumName}` | `Prisma{EnumName}` | なし |
| Relation | `{Type}WithIncludes` | `{Type}WithIncludes` (`@dto(nested)` で `{Type}ModelDto` 化) | なし |

nullable は `?` + `| null`、`toDto()` で `?.` + `?? null` 経由の安全変換。

### 11.5 アノテーション (`///` コメント)

#### `@json(type: [TypeName])` — Json フィールドにカスタム型

```prisma
model JsonField {
  id         Int  @id @default(autoincrement())
  rawJson    Json
  jsonObject Json /// @json(type: [JsonObject])
}
```

`additionalTypePath` 先で型 export:

```ts
// prisma/@additionalType/index.ts
export type JsonObject = { foo: string; bar: number };
```

`fromPrismaValue` 内では `as unknown as JsonObject` で cast。

#### `@dto(hidden: true)` — DTO から除外

```prisma
model User {
  password String /// @dto(hidden: true)
}
```

- `{Model}ModelDto` 型・`toDto()` 出力から除外
- `ConstructorArgs` / `fromPrismaValue` / private field / getter は **保持** (内部利用可)

#### `@dto(nested: true)` — 関連を `{Related}ModelDto` にネスト変換

```prisma
model User {
  posts Post[] /// @dto(nested: true)
  books Book[]
}
```

```ts
export type UserModelDto = {
  posts: PostModelDto[];        // nested → DTO
  books: BookWithIncludes[];    // 注釈なし → 生 Prisma 型
};

// toDto() 内で自動変換: posts.map(p => PostModel.builder().fromPrisma(p).build().toDto())
```

#### `@dto.profile(name: X, pick: [...] | omit: [...])` — 用途別 DTO

モデル直上に `///` コメントで宣言:

```prisma
/// @dto.profile(name: Public, pick: [id, email, name])
/// @dto.profile(name: Admin, omit: [password])
model User {
  id       Int    @id
  email    String @unique
  name     String?
  password String /// @dto(hidden: true)
}
```

生成: `UserPublicDto` / `UserAdminDto` 型 + `toPublicDto()` / `toAdminDto()` メソッド。

ルール:
- `pick` と `omit` は排他 (両方指定 → 当該プロファイル無視)
- profile は `@dto(hidden)` を **上書き** — `pick` で hidden field を明示指定すれば含まれる
- 存在しない field 名は warning 出力でスキップ
- 同名 profile 重複は最初のもののみ採用

#### 複合: 同一 field に複数アノテーション可

```prisma
settings Json /// @json(type: [SettingsObject]) @dto(hidden: true)
```

### 11.6 Builder パターン

`fromPrismaValue` は全リレーション必須。Builder は柔軟版 (リレーションスキップ可、テスト fixture 向け、サブクラス拡張可)。

```ts
// fromPrismaValue (厳格)
const user = UserModel.fromPrismaValue({ self, posts, books, notifications });  // 全関連必須

// builder (柔軟)
const user = UserModel.builder()
  .fromPrisma(prismaUser)         // スカラー全 set
  .posts(loadedPosts)              // 必要な関連だけ
  .build();                        // 未設定: list → [], optional → undefined
```

API:
- `fromPrisma(value: Prisma{Model})` — スカラー一括 set (型変換込み)
- `<scalarField>(value)` — 個別 setter (テスト fixture で Faker と組み合わせ向け)
- `<relationField>(value)` — 関連 setter
- `protected buildArgs()` — サブクラスから利用可
- `build(): {Model}Model` — 必須 field 未設定時は `Error`

`buildArgs()` デフォルト値:
- 必須 scalar 未設定 → throw
- optional scalar → `null`
- list relation → `[]`
- optional single relation → `undefined`
- 必須 single relation 未設定 → throw

**サブクラス拡張** (カスタム field 追加):

```ts
class AppUser extends UserModel {
  constructor(args: UserModelConstructorArgs & { fullName: string }) {
    super(args);
    this._fullName = args.fullName;
  }
  override toDto() { return { ...super.toDto(), fullName: this._fullName }; }
}

class AppUserBuilder extends UserModelBuilder {
  fullName(v: string): this { this._fullName = v; return this; }
  override build(): AppUser {
    return new AppUser({ ...this.buildArgs(), fullName: this._fullName });
  }
}
```

### 11.7 Repository Generator (Beta)

別ジェネレーターブロックで有効化:

```prisma
generator repository {
    provider  = "frourio-framework-prisma-repository-generator"
    output    = "__generated__/repository"
    modelPath = "__generated__/models"   // model generator の output と一致
}
```

生成物:
- `BaseRepository` — CRUD + ページネーション抽象クラス
- `{Model}Repository` — 各モデル具象クラス (自動 `findBy*` + `paginate`)

**自動生成メソッド** (schema メタデータから):

| ソース | メソッド | 例 |
|---|---|---|
| `@id` field | `findBy{Field}(value)` | `findById(id)` |
| `@unique` field | `findBy{Field}(value)` | `findByEmail(email)` |
| `@@unique([x, y])` | `findBy{X}And{Y}(x, y)` | `findByBookIdAndPostId(...)` |
| 全モデル | `paginate(args?)` | typed where/orderBy/page/perPage |

**継承メソッド** (BaseRepository): `findMany` / `findFirst` / `count` / `exists` / `create` / `createMany` / `update` / `updateMany` / `upsert` / `delete` / `deleteMany` / `aggregate` / `cursorPaginate` / `withTransaction(tx)`。

利用例:

```ts
const userRepo = new UserRepository(prisma.user);
const user = await userRepo.findById(1);
const page = await userRepo.paginate({
  page: 1, perPage: 20,
  where: { name: { contains: 'alice', mode: 'insensitive' } },
  orderBy: { field: 'id', direction: 'desc' },
});
```

カスタムメソッド追加は継承で:

```ts
import { UserRepository as Generated } from './__generated__/repository/User.repository';
export class UserRepository extends Generated {
  async findActive() { return this.findMany({ where: { active: true } }); }
}
```

### 11.8 共有型エクスポート (推奨)

frontend と DTO 型を単一ソース共有するため、共通 types エントリで re-export:

```ts
// shared-types/models.ts
export type { UserModelDto, UserPublicDto, UserAdminDto } from '<output>/User.model';
export type { PostModelDto } from '<output>/Post.model';
```

### 11.9 使用ルール (必須)

**A. `index.ts` の resBody は生成 DTO 型を使用** (自前再定義禁止)

```ts
import type { UserModelDto } from '<shared-types>';
export type Methods = DefineMethods<{ get: { resBody: UserModelDto } }>;
```

**B. controller は `.toDto()` 経由で返却**

```ts
get: async ({ params }) => {
  const user = await userRepo.findById(params.id);   // UserModel インスタンス
  return { status: 200, body: user.toDto() };
},
```

UseCase / repository が Prisma 値取得 → `<Model>.fromPrismaValue({ self, ...includes })` または `<Model>.builder().fromPrisma(...).build()` で Model 化 → controller が `.toDto()`。

**C. Prisma 生値・Model インスタンス直接返却禁止**

```ts
// ❌ Prisma 生値 (Date 型残留)
return { status: 200, body: await prisma.user.findUnique({ where: { id } }) };
// ❌ Model インスタンス (private field 露出, Date 残留)
return { status: 200, body: userModel };
// ✅
return { status: 200, body: userModel.toDto() };
```

**D. 用途別 DTO は profile 使用** — 同一 model から複数 DTO 形が必要なら `@dto.profile` で `toPublicDto()` 等を生成し、`index.ts` の resBody はそれぞれ `UserPublicDto` / `UserAdminDto` を使用。

**E. センシティブフィールドは `@dto(hidden: true)`** — `password` 等は schema で hidden 指定 → 標準 `toDto()` から自動除外。

### 11.10 生成コマンド

```bash
prisma generate    # 全 generator (model + repository) を同時起動
```

schema.prisma 変更後は必ず実行。
**生成ファイル編集禁止** (`__generated__/` 配下は次回生成で消える)。

## チェックリスト (新規ルート追加時)

- [ ] `index.ts` の `Methods` 型は実装と一致
- [ ] resBody は生成 `<Model>ModelDto` 型を使用 — 自前で型再定義しない
- [ ] 動的セグメントは `_name@type` 形式 (`@string` or `@number`)、`validators.ts` で zod 検証
- [ ] handler は `defineController` 経由 (素の関数 export 禁止)
- [ ] API 応答は Domain Model `.toDto()` (or `.to<Profile>Dto()`) 経由 — Prisma 生値・Model インスタンス直接返却禁止
- [ ] Prisma → Model 変換は `<Model>Model.fromPrismaValue({ self, ...includes })` または `<Model>Model.builder().fromPrisma(...).<rel>(...).build()` で行う
- [ ] センシティブ field (`password` 等) は schema で `/// @dto(hidden: true)` 指定済み
- [ ] 用途別 DTO が必要な場合は `/// @dto.profile(name: X, pick/omit: [...])` で生成し、自前型再定義しない
- [ ] `Json` フィールドの独自型は `/// @json(type: [TypeName])` + `additionalTypePath` 経由
- [ ] auth 要否 → `hooks.ts` で middleware 注入 (親 hooks.ts 継承確認)
- [ ] hooks ロジックの重複コピペ禁止 — 共通認証/middleware は `backend-api/middleware/` に切り出し、各 `hooks.ts` は `defineHooks(() => ({ onRequest: <imported fn> }))` 薄ラッパで import 再利用
- [ ] 認証必須範囲が複数ルートに渡るときは、共通祖先ディレクトリに `hooks.ts` を 1つだけ置いて階層継承で適用 (子孫ごとの個別 `hooks.ts` 不要)
- [ ] `$server.ts` / `$relay.ts` / Prisma 生成 `__generated__/` を手書き編集していない
- [ ] schema.prisma 変更時は `prisma generate` 実行 (model + repository 同時生成)
- [ ] `frourio` 実行 → 型エラー無し
- [ ] `prisma` と `@prisma/client` のバージョンが一致 (`>=7.2.0`)
- [ ] 新規 Model 追加時は共有 types エントリに `*ModelDto` (+ profile DTO) を re-export 追加
- [ ] frontend 側で `aspida` クライアント経由呼び出し時に型補完が効く
- [ ] **frontend のデータ取得は `useFrourioSWR` を使用** — `useAspidaSWR` は legacy fallback、`useEffect` + `fetch` 禁止
- [ ] 条件付き fetch は `useFrourioSWR` の endpoint 引数に `null` / `undefined` / `false` を渡す (enable 相当)
- [ ] 依存値変化での再取得は `useFrourioSWR` の query / params 経由 (キャッシュキー自動更新)

