# Frourio Framework Review

> frourio (Fastify + aspida + zod ベースのTypeScriptフルスタックフレームワーク) 採用プロジェクトの PR / 差分コードレビュー観点。違反検出規約・優先度 (Critical/Warning/Info)・ 検出パターンと修正案・自動生成ファイル編集検出・DTO 変換漏れ検出・ Prisma Model 変換違反検出・hooks 重複検出・useEffect データ取得検出・ センシティブ field 漏洩検出・useFrourioSWR 移行漏れ検出を網羅。 実装規約 skill (frourio-framework) の対となるレビュー観点 skill。 Triggers: "frourio レビュー", "frourio コードレビュー", "frourio PR レビュー", "frourio review", "controller レビュー", "DTO 漏れ", "toDto 検出", "hooks 重複", "useEffect 検出", "useFrourioSWR 移行漏れ", "$server.ts 編集", "$relay.ts 編集", "__generated__ 編集", "frourio-framework-review".

- Skill: `interfacex-co-jp/frourio-framework-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add interfacex-co-jp/frourio-framework-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/interfacex-co-jp/frourio-framework-review/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-review

---


# frourio Framework — Code Review

frourio (Fastify + aspida + zod) ベース TypeScript フルスタックフレームワーク採用プロジェクトの
PR / 差分レビュー観点を集約。
違反検出 → 指摘必須。1指摘1行 (位置・問題・修正)。

実装規約 (skill: `frourio-framework`) の鏡像。実装時規約 vs レビュー時検出観点で役割分離。

## 0. レビュー優先度

- 🔴 **Critical** — 型安全性・セキュリティ・自動生成破壊。即修正
- 🟡 **Warning** — 規約逸脱・保守性低下。修正推奨
- 🔵 **Info** — スタイル・命名。任意

## 1. ディレクトリ・ファイル配置 (🔴 Critical)

- [ ] 新規ルートに `index.ts` (Methods 型) 存在
- [ ] 新規ルートに `controller.ts` (defineController) 存在
- [ ] 動的セグメント (`_<name>@<type>/`) 含むルートに `validators.ts` 存在
- [ ] 動的セグメント命名: `_<name>@string` or `_<name>@number` のみ (他型禁止)
- [ ] ルートディレクトリ名と URL マッピングが意図通り

検出パターン:
- `api/foo/_id/` (型指定無し) → `_id@string` or `_id@number` に修正
- `api/foo/[id]/` (Next.js 風) → frourio 規約違反
- `api/foo/:id/` (Express 風) → frourio 規約違反

検出 grep:
```bash
# 型指定無し動的セグメント
find api -type d -name '_*' | grep -v '@string\|@number'
# Next.js 風 / Express 風
find api -type d \( -name '\[*\]' -o -name ':*' \)
```

## 2. 自動生成ファイル編集 (🔴 Critical)

以下が diff に含まれる → **即 reject**:
- `$server.ts` (プロジェクトルート)
- 各ルートの `$relay.ts`
- `api/$api.ts`
- `__generated__/` 配下全て (Prisma model + repository 生成物)

検出 grep:
```bash
git diff --name-only origin/main...HEAD | grep -E '(\$server\.ts|\$relay\.ts|\$api\.ts|__generated__/)'
```

修正方針: 該当ファイル変更を revert → schema / `index.ts` 等の **入力側** を修正 → `frourio` / `prisma generate` 再実行で再生成。

## 3. controller.ts (🔴 Critical)

- [ ] `export default defineController(...)` 形式のみ (素の関数 export 禁止)
- [ ] handler 引数 (`query`/`params`/`body`/`headers`) は `index.ts` Methods 定義と一致
- [ ] handler 戻り値 = `{ status, body, headers? }` シェイプ
- [ ] DI 利用時は `defineController({ ...deps }, ({ deps }, fastify) => ({...}))` overload
- [ ] async handler の throw → Fastify error handler でキャッチ可能か確認

違反例:
```ts
// ❌ 素の関数 export
export default async (req) => ({ status: 200, body: {} });

// ❌ status 欠落
get: () => ({ body: {} }),

// ❌ index.ts に未定義の query 参照
get: ({ query }) => { const x = query.foo; ... },  // Methods に foo 無し
```

検出 grep:
```bash
# defineController 経由でない default export
grep -rL 'defineController' api/**/controller.ts
```

## 4. API 応答 — DTO 変換 (🔴 Critical)

**最重要**: API レスポンスは必ず `<Model>Model.toDto()` 経由。

検出パターン (NG):
```ts
// ❌ Prisma 生値直接返却 — Date 型残留 → JSON シリアライズで型不整合
const user = await prisma.user.findUnique({ where: { id } });
return { status: 200, body: user };

// ❌ Model インスタンス直接返却 — private field 露出 + Date 残留
const user = UserModel.fromPrismaValue({ self: prismaUser });
return { status: 200, body: user };

// ❌ 手動 spread — toDto() の DTO 変換ロジック迂回
return { status: 200, body: { id: user.id, name: user.name } };

// ❌ 手動 omit (password 等) — @dto(hidden) で schema 側に寄せる
return { status: 200, body: { ...user.toDto(), password: undefined } };
```

OK パターン:
```ts
// ✅ toDto() 経由
return { status: 200, body: user.toDto() };

// ✅ 用途別 DTO
return { status: 200, body: user.toPublicDto() };

// ✅ 配列も map で
return { status: 200, body: users.map(u => u.toDto()) };
```

検出 grep:
```bash
# controller 内で prisma 直叩き → 即返却の臭い
grep -rn 'prisma\.\w\+\.\(findUnique\|findFirst\|findMany\|create\|update\)' api/**/controller.ts
# return body: <prisma変数> パターン
grep -rn 'body:\s*\(await\|prisma\)' api/**/controller.ts
```

## 5. Prisma → Model 変換 (🔴 Critical)

- [ ] `<Model>Model.fromPrismaValue({ self, ...includes })` または `<Model>Model.builder().fromPrisma(...).<rel>(...).build()` で変換
- [ ] `new <Model>Model(...)` 直接呼び禁止 (型変換ロジック迂回)
- [ ] `fromPrismaValue` 利用時は relation 全指定 (欠落 → ランタイムエラー)
- [ ] Builder 利用時の必須 field 設定漏れチェック

違反例:
```ts
// ❌ 直接 new
const user = new UserModel({ id, name, ... });

// ❌ fromPrismaValue で relation 欠落 (UserModel が posts/books 必須なのに self のみ)
const user = UserModel.fromPrismaValue({ self: prismaUser });
```

検出 grep:
```bash
grep -rn 'new \w\+Model(' src/ usecase/ api/
```

## 6. index.ts — 型定義 (🔴 Critical)

- [ ] `resBody` は生成 `<Model>ModelDto` 型を import 使用 (自前再定義禁止)
- [ ] `Methods` は `DefineMethods<{...}>` で wrap
- [ ] query / reqBody / resBody 型が controller 実装と一致

違反例:
```ts
// ❌ 自前型定義 — 生成 DTO とドリフト
export type Methods = DefineMethods<{
  get: { resBody: { id: number; name: string } };  // UserModelDto を使わない
}>;

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

検出 grep:
```bash
# resBody に inline object — DTO 型未使用の臭い
grep -rn 'resBody:\s*{' api/**/index.ts | grep -v 'ModelDto\|Dto\b'
```

## 7. validators.ts (🟡 Warning)

- [ ] 動的セグメント全 param が zod schema に含まれる
- [ ] `_id@number` → `z.coerce.number()` or `z.number()` (frourio 側で string→number 変換確認)
- [ ] `_id@string` → `z.string()` + 必要なら `.uuid()` / `.regex()` 等の追加検証
- [ ] reqBody / query 検証も必要なら追加 (信用境界)

検出 grep:
```bash
# 動的セグメント有り → validators.ts 欠落
for d in $(find api -type d -name '_*'); do
  parent=$(dirname "$d")
  [ -f "$parent/validators.ts" ] || echo "missing validators: $parent"
done
```

## 8. hooks.ts — auth (🔴 Critical)

- [ ] 認証必須ルート → `hooks.ts` で `onRequest` middleware 注入
- [ ] 親ディレクトリ `hooks.ts` での auth 注入 → 子孫ルートで重複定義不要 (継承確認)
- [ ] `req.user` 等のカスタム request property → `types/fastify.d.ts` で `declare module 'fastify'` 拡張済み
- [ ] auth 失敗時 `reply.code(401).send(...)` で early return
- [ ] controller 内で auth check しない (hooks に分離)
- [ ] **認証必須範囲 (例: `api/admin/` 配下) の `controller.ts` は、自身 or 親階層いずれかに `hooks.ts` 必須** — 自身も親も無い → 認証バイパス脆弱性 (🔴 即修正)
  - 例外: token 発行 endpoint (`api/admin/auth/login` 等) は意図的に親 hooks 適用範囲から外す

違反例:
```ts
// ❌ controller 内で auth check (hooks に分離すべき)
get: async (req) => {
  if (!req.headers.authorization) return { status: 401, body: {...} };
  ...
}

// ❌ req.user 型未拡張 → controller で型エラー or any 化
req.user = await req.jwtVerify();  // FastifyRequest に user property なし

// ❌ 認証範囲の hooks.ts 配置漏れ → バイパス
// api/admin/sync/controller.ts 存在、api/admin/sync/hooks.ts 無し、
// 親 api/admin/hooks.ts も無し (admin/auth/login のため共通祖先には置けない)
//   → 認証ゼロで sync trigger 叩ける = 脆弱性
```

検出 grep:
```bash
# controller 内 auth check (hooks 漏れ)
grep -rn 'headers\.\(authorization\|cookie\)' api/**/controller.ts
grep -rn 'jwtVerify\|verifyToken' api/**/controller.ts

# 認証必須範囲 (admin 配下) で controller.ts に hooks.ts 継承無し検出
# 自身ディレクトリと祖先全部を遡って hooks.ts 探索 → 1個も無ければ警告
for f in $(find api/admin -name controller.ts); do
  dir=$(dirname "$f")
  found=""
  while [ "$dir" != "api" ] && [ "$dir" != "." ] && [ "$dir" != "/" ]; do
    if [ -f "$dir/hooks.ts" ]; then found=1; break; fi
    dir=$(dirname "$dir")
  done
  [ -z "$found" ] && echo "AUTH BYPASS RISK: $f"
done
# login / callback 等 token 発行 endpoint は除外 (意図的バイパス)
```

## 9. hooks 重複 — middleware 切り出し (🟡 Warning)

- [ ] 兄弟 `hooks.ts` 間で同一ロジックのコピペ無し
- [ ] 共通認証 / rate limit / logging は `backend-api/middleware/<name>.ts` に集約
- [ ] 各 `hooks.ts` は import + `defineHooks(() => ({ onRequest: <imported fn> }))` の薄ラッパに留まる
- [ ] 認証必須範囲の **共通祖先** に `hooks.ts` を 1つだけ配置 (子孫ごと個別禁止)
- [ ] 認証範囲が異なる兄弟 (例: `auth/login` 等) は共通祖先より下に分離 (frourio hooks は重ねがけ・override 不可)
- [ ] middleware は `(req: FastifyRequest, reply: FastifyReply) => Promise<void>` 形式に統一 (Fastify インスタンス必要時は `req.server` 経由 — 例: `req.server.jwt.verify(token)`)
- [ ] 子孫に既に hooks 継承されているのに、子側で重ねて同じ middleware を再注入しない (二重実行 → reply 二度送信エラー)

### 9.1 配置パターン早見表

認証範囲とディレクトリ構成の典型パターン:

| 構成 | hooks.ts 配置 | 理由 |
|---|---|---|
| `admin/` 配下全部に同一認証 + `auth/login` のみ token 発行 | 各サブ範囲ごと: `admin/clients/hooks.ts`, `admin/report/hooks.ts`, `admin/sync/hooks.ts` (login は親に置けないので) | `admin/hooks.ts` 親集約 → login も認証必須化 → token発行不能 |
| `admin/` 配下全部に同一認証 + login 系無し | `admin/hooks.ts` 1個のみ | 共通祖先1個で全子孫継承 |
| ルート別に異なる scope (admin vs user) | `admin/hooks.ts` (admin scope) + `user/hooks.ts` (user scope) | scope ごとに祖先分離 |
| 一部だけ追加 hook (rate limit 等) | 既存 auth hooks.ts より下の階層に新規 hooks.ts (auth は祖先継承、rate limit は当該階層追加) | hooks 重ねがけ |

違反例:
```ts
// ❌ api/admin/clients/.../tiktok/hooks.ts と api/admin/report/hooks.ts に
//    同一の認証コード 60 行コピペ

// ❌ 親 admin/clients/hooks.ts で auth 適用済みなのに
//    子 admin/clients/_id@string/tiktok/hooks.ts で同じ authAdminMiddleware 再注入
//    → 二重実行で reply 二度送信 (FST_ERR_REP_ALREADY_SENT)
```

OK パターン:
```ts
// middleware/authAdminMiddleware.ts — 1箇所に集約 (fastify インスタンス引数なし)
import type { FastifyRequest, FastifyReply } from 'fastify';
export const authAdminMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
  const payload = await req.server.jwt.verify(token);  // req.server で fastify 取得
  if (!hasAdminScope(payload)) { reply.code(403).send(...); return reply; }
};

// api/admin/clients/hooks.ts — 共通祖先に 1つだけ (tiktok/, _id@string/ 子孫全部に継承)
import { defineHooks } from './$relay';
import { authAdminMiddleware } from '$/middleware/authAdminMiddleware';
export default defineHooks(() => ({ onRequest: authAdminMiddleware }));
```

検出 grep:
```bash
# hooks.ts ファイル内の行数が多い → middleware 切り出し漏れの臭い (薄ラッパなら 5行前後)
find api -name 'hooks.ts' -exec wc -l {} \; | awk '$1 > 15'

# 同一 import パターンの兄弟 hooks.ts カウント
grep -rh '^import' api/**/hooks.ts | sort | uniq -c | sort -rn | head

# 同一 middleware を親子両方の hooks.ts で onRequest 注入 → 二重実行検出
for f in $(find api -name hooks.ts); do
  dir=$(dirname "$f")
  imports=$(grep -oE 'from .+/middleware/\w+' "$f" | sort -u)
  parent=$(dirname "$dir")
  while [ "$parent" != "api" ] && [ "$parent" != "." ] && [ "$parent" != "/" ]; do
    if [ -f "$parent/hooks.ts" ]; then
      for imp in $imports; do
        if grep -q "$imp" "$parent/hooks.ts" 2>/dev/null; then
          echo "DUPLICATE INJECTION: $f also injects $imp already from $parent/hooks.ts"
        fi
      done
    fi
    parent=$(dirname "$parent")
  done
done

# defineHooks の中身が短い (薄ラッパ) か診断
grep -lE 'defineHooks\(\(\) => \(\{ onRequest: \w+ \}\)\)' api/**/hooks.ts
```

## 10. Prisma Schema — DTO アノテーション (🔴 Critical)

- [ ] センシティブ field (`password` / `apiKey` / `secret` / `token` 等) は `/// @dto(hidden: true)` 付与
- [ ] 用途別 DTO は `/// @dto.profile(name: X, pick/omit: [...])` で生成 (controller で自前選別禁止)
- [ ] `Json` field の独自型は `/// @json(type: [TypeName])` + generator の `additionalTypePath` 設定
- [ ] relation を nested DTO 化したい場合は `/// @dto(nested: true)`
- [ ] `@dto.profile` で `pick` と `omit` 同時指定無し (排他)

違反例:
```prisma
// ❌ password が hidden 指定無し → toDto() で漏洩
model User {
  password String
}

// ❌ controller で手動 omit (自前ロジック)
return { status: 200, body: { ...user.toDto(), password: undefined } };
```

検出 grep:
```bash
# センシティブ field 名 + @dto(hidden) 欠落
grep -nE '^\s+(password|apiKey|secret|token|refreshToken)\s+String' schema.prisma | \
  grep -v '@dto(hidden'
```

## 11. Prisma バージョン整合 (🔴 Critical)

- [ ] `prisma` と `@prisma/client` が **同一バージョン**、かつ **>= 7.2.0**
- [ ] `package.json` 差分でバージョンずれ検出時は指摘
- [ ] lockfile (`package-lock.json` / `pnpm-lock.yaml`) も整合

検出 bash:
```bash
node -e "
const p = require('./package.json');
const a = (p.dependencies||{})['@prisma/client'];
const b = (p.devDependencies||{})['prisma'] || (p.dependencies||{})['prisma'];
if (a !== b) console.log('VERSION MISMATCH:', { '@prisma/client': a, prisma: b });
"
```

## 12. 共有型エクスポート (🟡 Warning)

- [ ] 新規 Model 追加 → 共有 types エントリ (例: `shared-types/models.ts`) に `<Model>ModelDto` (+ profile DTO) を re-export 追加
- [ ] frontend / backend 双方が同一 import path で DTO 型参照
- [ ] `__generated__/` 配下を frontend が直 import していない (共有 entry 経由)

検出 grep:
```bash
# frontend 側で __generated__ 直 import
grep -rn '__generated__' frontend/src/ web/src/
```

## 13. フロントエンド — データ取得 (🔴 Critical)

- [ ] データ取得は `useFrourioSWR` を使用
- [ ] `useAspidaSWR` 新規利用 → `useFrourioSWR` に置換指摘 (legacy fallback)
- [ ] **`useEffect` + `fetch` / `aspida client` 直叩き禁止**
- [ ] 条件付き fetch は `useFrourioSWR(condition ? endpoint : null)` パターン
- [ ] 依存値変化での再取得は query / params 経由 (キャッシュキー自動更新)
- [ ] mutation 後の再取得は `mutate()` 経由
- [ ] `useState` + `useEffect` で fetch 結果を保持していないか

違反例:
```ts
// ❌ useEffect でデータ取得
useEffect(() => {
  adminApiClient.admin.users.$get().then(setUsers);
}, []);

// ❌ 自前 enable フラグ + useEffect
const [data, setData] = useState();
useEffect(() => {
  if (!id) return;
  adminApiClient.admin.tenants._tenantId(id).$get().then(setData);
}, [id]);

// ❌ useAspidaSWR 新規利用 (legacy)
const { data } = useAspidaSWR(adminApiClient.admin.users);
```

OK パターン:
```ts
// ✅ シンプル GET
const { data } = useFrourioSWR(adminApiClient.admin.users);

// ✅ 条件付き
const { data } = useFrourioSWR(
  id ? adminApiClient.admin.tenants._tenantId(id) : null,
);

// ✅ query
const { data } = useFrourioSWR(adminApiClient.hq.projects, {
  query: { status: 'IN_PROGRESS' },
});
```

検出 grep:
```bash
# useEffect 内で aspida client / fetch
grep -rnB1 -A5 'useEffect' frontend/src/ | grep -E '(\.\$get\(|\.\$post\(|fetch\()'
# useAspidaSWR 新規利用
grep -rn 'useAspidaSWR' frontend/src/
```

## 14. 生成コマンド実行漏れ (🟡 Warning)

- [ ] `schema.prisma` 変更 PR → `prisma generate` 実行済み (`__generated__/` 差分含む)
- [ ] 新規ルート / Methods 型変更 PR → frourio 生成実行済み (`$server.ts` / `$relay.ts` 整合)
- [ ] aspida 型変更 → `$api.ts` 整合

検出 bash:
```bash
# schema 変更されたが __generated__ 未更新
git diff --name-only origin/main...HEAD | grep -q 'schema\.prisma' && \
  ! git diff --name-only origin/main...HEAD | grep -q '__generated__/' && \
  echo "WARN: schema 変更あり、__generated__ 未更新 → prisma generate 漏れ"

# index.ts 変更あるが $server.ts / $relay.ts 未更新
git diff --name-only origin/main...HEAD | grep -q 'api/.*/index\.ts' && \
  ! git diff --name-only origin/main...HEAD | grep -qE '\$server\.ts|\$relay\.ts' && \
  echo "WARN: index.ts 変更あり、生成物未更新 → frourio 生成漏れの可能性"
```

## 15. 型エラー / ビルド (🔴 Critical)

- [ ] `tsc --noEmit` パス
- [ ] `npm run generate` 実行後の生成物と diff 整合 (生成漏れ検出)
- [ ] CI / lint pass

検出 bash:
```bash
npx tsc --noEmit
npm run generate && git diff --exit-code   # 生成物 drift 検出
```

## 16. レビュー実行手順

1. `git diff --name-only origin/main...HEAD` で差分ファイル一覧
2. **セクション 2** (自動生成ファイル編集) — 即 reject 判定
3. **セクション 1, 3-6, 8, 10, 11, 13, 15** (🔴 Critical) — 全数チェック
4. **セクション 7, 9, 12, 14** (🟡 Warning) — サンプリング or 全数
5. 検出 → genshijin-review 形式で出力

## レビュー出力フォーマット (genshijin-review 互換)

```
[file:line] 🔴/🟡/🔵 [問題]: [修正案]
```

例:
```
api/users/_id@string/controller.ts:12 🔴 Prisma 生値直接返却: user.toDto() に変更
api/users/index.ts:5 🔴 自前 resBody 型定義: UserModelDto を import 使用
prisma/schema.prisma:34 🔴 password に @dto(hidden: true) 欠落: コメント追加
frontend/pages/users.tsx:18 🔴 useEffect でデータ取得: useFrourioSWR に置換
$server.ts:1 🔴 自動生成ファイル編集: 元に戻し frourio CLI 再実行
api/admin/report/hooks.ts:5-65 🟡 hooks ロジックコピペ: middleware/authAdminMiddleware に切り出し
api/foo/_id/index.ts 🔴 動的セグメント型指定欠落: _id@string or _id@number に rename
package.json:12 🔴 prisma バージョン不整合: prisma と @prisma/client を同一バージョン (>= 7.2.0) に揃える
```

## 関連 skill

- 実装規約: `frourio-framework` skill (`skills/frourio-framework/SKILL.md`)
- 実装規約 (rule 形式): `rules/frourio-framework-activate.md`
- レビュー規約 (rule 形式): `rules/frourio-framework-review.md`

