@einja/dev-cli 0.1.48 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/presets/default/.claude/agents/einja/backend-architect.md +27 -17
- package/presets/default/.claude/skills/einja-coding-standards/references/testing-strategy.md +3 -3
- package/presets/default/docs/einja/steering/architecture.md +7 -1
- package/presets/default/docs/einja/steering/development/api-development.md +199 -67
- package/presets/default/docs/einja/steering/development/backend-architecture.md +12 -16
- package/presets/default/docs/einja/steering/development/frontend-development.md +61 -50
- package/presets/default/docs/einja/steering/development/review-guidelines.md +4 -1
- package/presets/default/docs/einja/steering/development/testing-strategy.md +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var _a;
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import chalk from "chalk";
|
|
4
7
|
import { Command } from "commander";
|
|
5
8
|
import { initCommand } from "./commands/init.js";
|
|
6
9
|
import { listCommand } from "./commands/list.js";
|
|
7
10
|
import { syncCommand } from "./commands/sync.js";
|
|
8
11
|
import { taskLoopCommand } from "./commands/task-loop/index.js";
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"));
|
|
9
15
|
const program = new Command();
|
|
10
16
|
// 旧パッケージ名での実行時に非推奨警告を表示
|
|
11
17
|
const isLegacyPackageName = (_a = process.argv[1]) === null || _a === void 0 ? void 0 : _a.includes("@einja/claude-cli");
|
|
@@ -16,7 +22,7 @@ if (isLegacyPackageName) {
|
|
|
16
22
|
program
|
|
17
23
|
.name("einja")
|
|
18
24
|
.description("Einja CLI - .claude設定とテンプレート同期をnpxでインストール")
|
|
19
|
-
.version(
|
|
25
|
+
.version(pkg.version);
|
|
20
26
|
program
|
|
21
27
|
.command("init")
|
|
22
28
|
.description(".claudeディレクトリをセットアップ")
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,wBAAwB;AACxB,MAAM,mBAAmB,GAAG,MAAA,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,0CAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;AAC3E,IAAI,mBAAmB,EAAE,CAAC;IACxB,OAAO,CAAC,IAAI,CACV,KAAK,CAAC,MAAM,CACV,+DAA+D,CAChE,CACF,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,oCAAoC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,OAAO;KACJ,IAAI,CAAC,OAAO,CAAC;KACb,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -93,13 +93,15 @@ graph TD
|
|
|
93
93
|
|
|
94
94
|
**実装例**:
|
|
95
95
|
```typescript
|
|
96
|
-
// apps/web/src/app/api/posts/route.ts
|
|
96
|
+
// apps/web/src/app/api/rpc/posts/[[...route]]/route.ts
|
|
97
97
|
import { Hono } from "hono"
|
|
98
|
+
import { handle } from "hono/vercel"
|
|
98
99
|
import { zValidator } from "@hono/zod-validator"
|
|
99
100
|
import { postSchema } from "@repo/server-core/domain/validators/post"
|
|
100
101
|
import { postUseCases } from "@/application/use-cases/PostUseCases"
|
|
101
102
|
|
|
102
103
|
const app = new Hono()
|
|
104
|
+
.basePath("/api/rpc/posts")
|
|
103
105
|
.post("/", zValidator("json", postSchema), async (c) => {
|
|
104
106
|
const data = c.req.valid("json")
|
|
105
107
|
const result = await postUseCases.create(data)
|
|
@@ -111,8 +113,10 @@ const app = new Hono()
|
|
|
111
113
|
return c.json(result.value, 201)
|
|
112
114
|
})
|
|
113
115
|
|
|
114
|
-
export
|
|
115
|
-
|
|
116
|
+
export type PostsAppType = typeof app
|
|
117
|
+
|
|
118
|
+
export const GET = handle(app)
|
|
119
|
+
export const POST = handle(app)
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
##### 📘 Application層(UseCases)
|
|
@@ -602,37 +606,43 @@ Honoでは、**必ずメソッドチェーン形式**でルートを定義しま
|
|
|
602
606
|
|
|
603
607
|
**重要: メソッドチェーンを使用する理由**
|
|
604
608
|
|
|
605
|
-
Hono Clientの型推論は `typeof app` から型情報を抽出します。メソッドチェーンを使用しない場合、TypeScript
|
|
609
|
+
Hono Clientの型推論は `typeof app` から型情報を抽出します。メソッドチェーンを使用しない場合、TypeScriptが各ルート定義の返り値型を追跡できず、ドメイン型(例: `PostsAppType`)に完全なルート情報が含まれません。
|
|
606
610
|
|
|
607
611
|
```typescript
|
|
608
612
|
// ❌ NG: 個別呼び出し - 型推論が損なわれる
|
|
609
613
|
const app = new Hono()
|
|
610
614
|
app.get('/posts', handler1) // 返り値が破棄される
|
|
611
615
|
app.post('/posts', handler2) // 返り値が破棄される
|
|
612
|
-
export type
|
|
616
|
+
export type PostsAppType = typeof app // ルート情報が不完全
|
|
613
617
|
|
|
614
618
|
// ✅ OK: メソッドチェーン - 完全な型推論
|
|
615
619
|
const app = new Hono()
|
|
616
|
-
.
|
|
617
|
-
.
|
|
618
|
-
|
|
620
|
+
.basePath("/api/posts")
|
|
621
|
+
.get('/', handler1)
|
|
622
|
+
.post('/', handler2)
|
|
623
|
+
export type PostsAppType = typeof app // 全ルート情報を含む
|
|
619
624
|
```
|
|
620
625
|
|
|
621
626
|
#### ミドルウェア適用
|
|
622
627
|
|
|
623
|
-
|
|
628
|
+
**ドメインベースRPC分割では、各ドメインのroute.tsで直接ミドルウェアを適用します。**
|
|
624
629
|
|
|
625
630
|
```typescript
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
631
|
+
// apps/web/src/app/api/admin/users/[[...route]]/route.ts
|
|
632
|
+
import { Hono } from "hono"
|
|
633
|
+
import { handle } from "hono/vercel"
|
|
634
|
+
import { adminAuthMiddleware } from "@/middleware/admin-auth"
|
|
630
635
|
|
|
631
|
-
// ✅ OK: メインアプリ側で.use()を適用
|
|
632
636
|
const app = new Hono()
|
|
633
|
-
.basePath("/api")
|
|
634
|
-
.use("
|
|
635
|
-
.
|
|
637
|
+
.basePath("/api/rpc/admin/users")
|
|
638
|
+
.use("/*", adminAuthMiddleware) // ドメインroute.ts内で適用
|
|
639
|
+
.get("/", listUsersHandler)
|
|
640
|
+
.delete("/:id", deleteUserHandler)
|
|
641
|
+
|
|
642
|
+
export type AdminUsersAppType = typeof app
|
|
643
|
+
|
|
644
|
+
export const GET = handle(app)
|
|
645
|
+
export const DELETE = handle(app)
|
|
636
646
|
```
|
|
637
647
|
|
|
638
648
|
### 7. Zodバリデーション戦略
|
package/presets/default/.claude/skills/einja-coding-standards/references/testing-strategy.md
CHANGED
|
@@ -425,7 +425,7 @@ describe('UserRepository - データベースエラー', () => {
|
|
|
425
425
|
|
|
426
426
|
```typescript
|
|
427
427
|
// apps/web/__tests__/integration/post-api.test.ts
|
|
428
|
-
import {
|
|
428
|
+
import { rpc } from '@/lib/api/rpc'
|
|
429
429
|
import { prisma } from '@repo/server-core/infrastructure/database/client'
|
|
430
430
|
|
|
431
431
|
describe('Post API統合テスト', () => {
|
|
@@ -454,7 +454,7 @@ describe('Post API統合テスト', () => {
|
|
|
454
454
|
})
|
|
455
455
|
|
|
456
456
|
// When: API呼び出し
|
|
457
|
-
const response = await
|
|
457
|
+
const response = await rpc.posts.$get({
|
|
458
458
|
query: { page: '1', limit: '10' }
|
|
459
459
|
})
|
|
460
460
|
|
|
@@ -475,7 +475,7 @@ describe('Post API統合テスト', () => {
|
|
|
475
475
|
})
|
|
476
476
|
|
|
477
477
|
// When: 投稿作成APIを呼び出し
|
|
478
|
-
const response = await
|
|
478
|
+
const response = await rpc.posts.$post({
|
|
479
479
|
json: {
|
|
480
480
|
title: 'New Post',
|
|
481
481
|
content: 'Content',
|
|
@@ -92,17 +92,23 @@ project-root/
|
|
|
92
92
|
│ ├── web/ # ユーザー向けWebアプリ
|
|
93
93
|
│ │ ├── src/
|
|
94
94
|
│ │ │ ├── app/ # Next.js App Router
|
|
95
|
+
│ │ │ │ └── api/rpc/ # ドメインベースRPC分割
|
|
96
|
+
│ │ │ │ └── {domain}/[[...route]]/route.ts
|
|
95
97
|
│ │ │ ├── application/ # アプリケーション層(UseCases)⭐
|
|
96
98
|
│ │ │ ├── components/ # Reactコンポーネント
|
|
97
|
-
│ │ │ ├── lib/ #
|
|
99
|
+
│ │ │ ├── lib/ # ライブラリ
|
|
100
|
+
│ │ │ │ └── rpc.ts # ドメインごとのHono RPCクライアント
|
|
98
101
|
│ │ │ └── hooks/ # カスタムフック(Tanstack Query)
|
|
99
102
|
│ │ └── package.json
|
|
100
103
|
│ ├── admin/ # 管理画面
|
|
101
104
|
│ │ ├── src/
|
|
102
105
|
│ │ │ ├── app/
|
|
106
|
+
│ │ │ │ └── api/rpc/ # ドメインベースRPC分割
|
|
107
|
+
│ │ │ │ └── {domain}/[[...route]]/route.ts
|
|
103
108
|
│ │ │ ├── application/ # アプリケーション層(UseCases)⭐
|
|
104
109
|
│ │ │ ├── components/
|
|
105
110
|
│ │ │ ├── lib/
|
|
111
|
+
│ │ │ │ └── rpc.ts # ドメインごとのHono RPCクライアント
|
|
106
112
|
│ │ │ └── hooks/
|
|
107
113
|
│ │ └── package.json
|
|
108
114
|
│ └── cron-worker/ # バッチ処理・定期実行
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- [メソッドチェーンパターン](#メソッドチェーンパターン)
|
|
16
16
|
- [ミドルウェアと型推論の注意点](#ミドルウェアと型推論の注意点) ⚠️
|
|
17
17
|
- [basePathとHono Clientの関係](#basepathとhono-clientの関係) ⚠️
|
|
18
|
+
- [ドメインベースRPC分割の設計原則](#ドメインベースrpc分割の設計原則)
|
|
18
19
|
2. [Web APIエンドポイント一覧](#2-web-apiエンドポイント一覧)
|
|
19
20
|
3. [Admin APIエンドポイント一覧](#3-admin-apiエンドポイント一覧)
|
|
20
21
|
4. [Cron Worker CLIコマンド](#4-cron-worker-cliコマンド)
|
|
@@ -34,10 +35,51 @@
|
|
|
34
35
|
|
|
35
36
|
Honoは型安全なWebフレームワークで、すべてのNext.js APIルートで使用します。
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
**エントリーポイント**(ドメインベースRPC分割):
|
|
38
39
|
```
|
|
39
|
-
apps/web/app/api/[[...route]]/route.ts # Web API
|
|
40
|
-
apps/admin/app/api/[[...route]]/route.ts # Admin API
|
|
40
|
+
apps/web/app/api/rpc/{domain}/[[...route]]/route.ts # Web API(ドメインごとに分割)
|
|
41
|
+
apps/admin/app/api/rpc/{domain}/[[...route]]/route.ts # Admin API(ドメインごとに分割)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**ドメインroute.tsテンプレート**:
|
|
45
|
+
|
|
46
|
+
各ドメインのroute.tsは以下のパターンで実装します。`basePath`でドメインのフルパスを指定し、`hc<...>("/")`で型ツリーからドメインクライアントを抽出します。
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// apps/web/app/api/rpc/users/[[...route]]/route.ts
|
|
50
|
+
import { userRoutes } from "@web/server/presentation/routes/userRoutes";
|
|
51
|
+
import { Hono } from "hono";
|
|
52
|
+
import { handle } from "hono/vercel";
|
|
53
|
+
|
|
54
|
+
const app = new Hono().basePath("/api/rpc/users");
|
|
55
|
+
const routes = app.route("/", userRoutes);
|
|
56
|
+
|
|
57
|
+
export type UsersAppType = typeof routes;
|
|
58
|
+
|
|
59
|
+
export const GET = handle(app);
|
|
60
|
+
export const POST = handle(app);
|
|
61
|
+
export const PUT = handle(app);
|
|
62
|
+
export const DELETE = handle(app);
|
|
63
|
+
export const PATCH = handle(app);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**RPCクライアントのセットアップ**:
|
|
67
|
+
|
|
68
|
+
> **Note**: 以下は複数ドメインを追加した場合の完成形の例です。現在のテンプレートでは `users` ドメインのみ実装されています。
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// apps/web/src/lib/api/rpc.ts
|
|
72
|
+
import type { AuthAppType } from "@/app/api/rpc/auth/[[...route]]/route";
|
|
73
|
+
import type { PostsAppType } from "@/app/api/rpc/posts/[[...route]]/route";
|
|
74
|
+
import { hc } from "hono/client";
|
|
75
|
+
|
|
76
|
+
const authClient = hc<AuthAppType>("/");
|
|
77
|
+
const postsClient = hc<PostsAppType>("/");
|
|
78
|
+
|
|
79
|
+
export const rpc = {
|
|
80
|
+
auth: authClient.api.rpc.auth,
|
|
81
|
+
posts: postsClient.api.rpc.posts,
|
|
82
|
+
} as const;
|
|
41
83
|
```
|
|
42
84
|
|
|
43
85
|
**ルート定義の配置**:
|
|
@@ -59,45 +101,70 @@ Hono Clientの型推論は `typeof app` から型情報を抽出します。メ
|
|
|
59
101
|
const app = new Hono()
|
|
60
102
|
app.get('/posts', handler1) // 返り値が破棄される
|
|
61
103
|
app.post('/posts', handler2) // 返り値が破棄される
|
|
62
|
-
export type
|
|
104
|
+
export type PostsAppType = typeof app // ルート情報が不完全
|
|
63
105
|
|
|
64
106
|
// ✅ OK: メソッドチェーン - 完全な型推論
|
|
65
107
|
const app = new Hono()
|
|
66
108
|
.get('/posts', handler1)
|
|
67
109
|
.post('/posts', handler2)
|
|
68
|
-
export type
|
|
110
|
+
export type PostsAppType = typeof app // 全ルート情報を含む
|
|
69
111
|
```
|
|
70
112
|
|
|
71
|
-
メソッドチェーンにより、Hono Client (`hc<
|
|
113
|
+
メソッドチェーンにより、Hono Client (`hc<PostsAppType>`) でエンドツーエンドの型安全なAPI呼び出しが実現できます。
|
|
72
114
|
|
|
73
115
|
### ミドルウェアと型推論の注意点
|
|
74
116
|
|
|
75
|
-
**サブルート内で`.use()
|
|
117
|
+
**サブルート内で`.use()`を使うと型推論が壊れる。各ドメインroute.tsのアプリ側で適用すること。**
|
|
118
|
+
|
|
119
|
+
ドメインベースRPC分割では、各ドメインのroute.tsが独立したHonoアプリを持つため、ミドルウェアはそのアプリに直接適用します。
|
|
76
120
|
|
|
77
121
|
```typescript
|
|
78
122
|
// ❌ NG: サブルート内で.use() → 型が ClientRequest<{}> になる
|
|
79
|
-
export const
|
|
80
|
-
.use("*",
|
|
81
|
-
.
|
|
123
|
+
export const postRoutes = new Hono()
|
|
124
|
+
.use("*", authMiddleware)
|
|
125
|
+
.post("/", handler)
|
|
82
126
|
|
|
83
|
-
// ✅ OK:
|
|
127
|
+
// ✅ OK: ドメインroute.tsのアプリ側で.use()を適用
|
|
128
|
+
// apps/web/app/api/rpc/posts/[[...route]]/route.ts
|
|
84
129
|
const app = new Hono()
|
|
85
|
-
.basePath("/api")
|
|
86
|
-
.use("
|
|
87
|
-
|
|
130
|
+
.basePath("/api/rpc/posts")
|
|
131
|
+
.use("/*", authMiddleware); // ← 認証必要なドメインではここで適用
|
|
132
|
+
const routes = app.route("/", postRoutes);
|
|
133
|
+
export type PostsAppType = typeof routes;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**ドメインごとのミドルウェア適用パターン**:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// 認証不要のドメイン(auth等)
|
|
140
|
+
const app = new Hono().basePath("/api/rpc/auth");
|
|
141
|
+
const routes = app.route("/", authRoutes);
|
|
142
|
+
export type AuthAppType = typeof routes;
|
|
143
|
+
|
|
144
|
+
// 認証必要のドメイン(posts等)
|
|
145
|
+
const app = new Hono()
|
|
146
|
+
.basePath("/api/rpc/posts")
|
|
147
|
+
.use("/*", authMiddleware);
|
|
148
|
+
const routes = app.route("/", postRoutes);
|
|
149
|
+
export type PostsAppType = typeof routes;
|
|
88
150
|
```
|
|
89
151
|
|
|
90
152
|
### basePathとHono Clientの関係
|
|
91
153
|
|
|
92
|
-
|
|
154
|
+
ドメインベースRPC分割では`basePath`で**ドメインのフルパスを指定**する。`hono/vercel`の`handle()`関数は完全なURLパスをHonoに渡すため、Honoがルートを正しくマッチするには`basePath`が必要。
|
|
93
155
|
|
|
94
156
|
```typescript
|
|
95
|
-
// サーバー: basePath
|
|
96
|
-
const app = new Hono().basePath("/api/rpc
|
|
157
|
+
// サーバー: basePathでドメインのフルパスを指定
|
|
158
|
+
const app = new Hono().basePath("/api/rpc/users");
|
|
159
|
+
const routes = app.route("/", userRoutes);
|
|
160
|
+
export type UsersAppType = typeof routes;
|
|
97
161
|
|
|
98
|
-
// クライアント:
|
|
99
|
-
|
|
100
|
-
|
|
162
|
+
// クライアント: hc("/")で型ツリーを構築し、ドメイン部分を抽出
|
|
163
|
+
import { hc } from "hono/client";
|
|
164
|
+
const client = hc<UsersAppType>("/");
|
|
165
|
+
const usersRpc = client.api.rpc.users;
|
|
166
|
+
|
|
167
|
+
usersRpc.$get() // ✅ OK(/api/rpc/users にリクエスト)
|
|
101
168
|
```
|
|
102
169
|
|
|
103
170
|
**使用例**:
|
|
@@ -128,6 +195,30 @@ app
|
|
|
128
195
|
export default app
|
|
129
196
|
```
|
|
130
197
|
|
|
198
|
+
### ドメインベースRPC分割の設計原則
|
|
199
|
+
|
|
200
|
+
#### なぜドメインで分割するか
|
|
201
|
+
|
|
202
|
+
1. **Vercel Function独立**: 各ドメインが独立したServerless Functionとしてデプロイされるため、障害の影響範囲が限定される
|
|
203
|
+
2. **コールドスタート改善**: 各Functionのバンドルサイズが小さくなり、コールドスタート時間が短縮される
|
|
204
|
+
3. **スケーラビリティ**: トラフィックの多いドメイン(例: posts)だけが独立してスケールできる
|
|
205
|
+
|
|
206
|
+
#### ドメイングループ設計の基準
|
|
207
|
+
|
|
208
|
+
ドメインの分割は**依存の重さ・頻度**で分類する。
|
|
209
|
+
|
|
210
|
+
| 基準 | 説明 | 例 |
|
|
211
|
+
|------|------|-----|
|
|
212
|
+
| 依存の独立性 | 外部サービスやDB接続が異なる | auth(認証サービス依存)vs posts(DB依存) |
|
|
213
|
+
| リクエスト頻度 | 高頻度ドメインを分離して他に影響させない | analytics(低頻度)vs posts(高頻度) |
|
|
214
|
+
| ミドルウェアの違い | 認証要否など共通処理が異なる | auth(認証不要)vs users(認証必要) |
|
|
215
|
+
|
|
216
|
+
#### 新ドメイン追加手順
|
|
217
|
+
|
|
218
|
+
1. **route.ts作成**: `app/api/rpc/{domain}/[[...route]]/route.ts` にドメインroute.tsテンプレートを配置
|
|
219
|
+
2. **rpc.tsに追加**: `lib/api/rpc.ts` の `rpc` オブジェクトに新ドメインのクライアントを追加
|
|
220
|
+
3. **フック作成**: 必要に応じて `hooks/api/use{Domain}*.ts` にTanstack Queryフックを作成
|
|
221
|
+
|
|
131
222
|
### zValidatorの統合
|
|
132
223
|
|
|
133
224
|
**必須**: すべてのリクエストボディとクエリパラメータは、zValidatorでバリデーションを行う。
|
|
@@ -176,23 +267,23 @@ async (c) => {
|
|
|
176
267
|
|---------|---------------|------|-----------|-----------|------|
|
|
177
268
|
| GET | `/api/health` | システム稼働確認 | - | `{status: "ok"}` | 不要 |
|
|
178
269
|
|
|
179
|
-
###
|
|
270
|
+
### 認証エンドポイント(ドメイン: auth)
|
|
180
271
|
|
|
181
272
|
| メソッド | エンドポイント | 説明 | リクエスト | レスポンス | 認証 |
|
|
182
273
|
|---------|---------------|------|-----------|-----------|------|
|
|
183
|
-
| POST | `/api/auth/login` | ユーザーログイン | `{email, password}` | `{token, user}` | 不要 |
|
|
184
|
-
| POST | `/api/auth/logout` | ログアウト | - | `{success: true}` | 必要 |
|
|
185
|
-
| GET | `/api/auth/session` | セッション確認 | - | `{user}` | 必要 |
|
|
274
|
+
| POST | `/api/rpc/auth/login` | ユーザーログイン | `{email, password}` | `{token, user}` | 不要 |
|
|
275
|
+
| POST | `/api/rpc/auth/logout` | ログアウト | - | `{success: true}` | 必要 |
|
|
276
|
+
| GET | `/api/rpc/auth/session` | セッション確認 | - | `{user}` | 必要 |
|
|
186
277
|
|
|
187
|
-
###
|
|
278
|
+
### 投稿エンドポイント(ドメイン: posts)
|
|
188
279
|
|
|
189
280
|
| メソッド | エンドポイント | 説明 | リクエスト | レスポンス | 認証 |
|
|
190
281
|
|---------|---------------|------|-----------|-----------|------|
|
|
191
|
-
| GET | `/api/posts` | 投稿一覧取得 | `?page=1&limit=10` | `{posts[], total}` | オプション |
|
|
192
|
-
| POST | `/api/posts` | 投稿作成 | `{title, content, status?}` | `{post}` | 必要 |
|
|
193
|
-
| GET | `/api/posts/:id` | 投稿詳細取得 | - | `{post}` | オプション |
|
|
194
|
-
| PUT | `/api/posts/:id` | 投稿更新 | `{title?, content?, status?}` | `{post}` | 必要 |
|
|
195
|
-
| DELETE | `/api/posts/:id` | 投稿削除 | - | `{success: true}` | 必要 |
|
|
282
|
+
| GET | `/api/rpc/posts` | 投稿一覧取得 | `?page=1&limit=10` | `{posts[], total}` | オプション |
|
|
283
|
+
| POST | `/api/rpc/posts` | 投稿作成 | `{title, content, status?}` | `{post}` | 必要 |
|
|
284
|
+
| GET | `/api/rpc/posts/:id` | 投稿詳細取得 | - | `{post}` | オプション |
|
|
285
|
+
| PUT | `/api/rpc/posts/:id` | 投稿更新 | `{title?, content?, status?}` | `{post}` | 必要 |
|
|
286
|
+
| DELETE | `/api/rpc/posts/:id` | 投稿削除 | - | `{success: true}` | 必要 |
|
|
196
287
|
|
|
197
288
|
**ページネーション設計**:
|
|
198
289
|
- `page`: ページ番号(デフォルト: 1)
|
|
@@ -214,27 +305,27 @@ async (c) => {
|
|
|
214
305
|
|---------|---------------|------|-----------|-----------|------|
|
|
215
306
|
| GET | `/api/health` | システム稼働確認 | - | `{status: "ok"}` | 不要 |
|
|
216
307
|
|
|
217
|
-
###
|
|
308
|
+
### ユーザー管理(ドメイン: users)
|
|
218
309
|
|
|
219
310
|
| メソッド | エンドポイント | 説明 | リクエスト | レスポンス | 認証 |
|
|
220
311
|
|---------|---------------|------|-----------|-----------|------|
|
|
221
|
-
| GET | `/api/
|
|
222
|
-
| GET | `/api/
|
|
223
|
-
| PUT | `/api/
|
|
224
|
-
| DELETE | `/api/
|
|
312
|
+
| GET | `/api/rpc/users` | ユーザー一覧取得 | `?page=1&limit=20` | `{users[], total}` | 管理者 |
|
|
313
|
+
| GET | `/api/rpc/users/:id` | ユーザー詳細取得 | - | `{user}` | 管理者 |
|
|
314
|
+
| PUT | `/api/rpc/users/:id` | ユーザー情報更新 | `{name?, email?}` | `{user}` | 管理者 |
|
|
315
|
+
| DELETE | `/api/rpc/users/:id` | ユーザー削除 | - | `{success: true}` | 管理者 |
|
|
225
316
|
|
|
226
|
-
###
|
|
317
|
+
### 投稿管理(ドメイン: posts)
|
|
227
318
|
|
|
228
319
|
| メソッド | エンドポイント | 説明 | リクエスト | レスポンス | 認証 |
|
|
229
320
|
|---------|---------------|------|-----------|-----------|------|
|
|
230
|
-
| GET | `/api/
|
|
231
|
-
| PUT | `/api/
|
|
321
|
+
| GET | `/api/rpc/posts` | 全投稿一覧取得 | `?status=all&page=1` | `{posts[], total}` | 管理者 |
|
|
322
|
+
| PUT | `/api/rpc/posts/:id/status` | 投稿ステータス変更 | `{status}` | `{post}` | 管理者 |
|
|
232
323
|
|
|
233
|
-
###
|
|
324
|
+
### 分析(ドメイン: analytics)
|
|
234
325
|
|
|
235
326
|
| メソッド | エンドポイント | 説明 | リクエスト | レスポンス | 認証 |
|
|
236
327
|
|---------|---------------|------|-----------|-----------|------|
|
|
237
|
-
| GET | `/api/
|
|
328
|
+
| GET | `/api/rpc/analytics` | システム統計取得 | `?from=&to=` | `{stats}` | 管理者 |
|
|
238
329
|
|
|
239
330
|
**管理者認証**:
|
|
240
331
|
- すべてのAdmin APIは、管理者権限(`role='admin'`)のチェックが必要
|
|
@@ -567,13 +658,13 @@ Hono Client + Tanstack Queryでは、`parseResponse`関数を使用してレス
|
|
|
567
658
|
import { useQuery } from "@tanstack/react-query";
|
|
568
659
|
import { parseResponse } from "@/lib/api/parse-response";
|
|
569
660
|
import { paginatedPostListSchema } from "@/shared/schemas/post";
|
|
570
|
-
import {
|
|
661
|
+
import { rpc } from "@/lib/api/rpc";
|
|
571
662
|
|
|
572
663
|
export function usePostList(page: number, limit: number) {
|
|
573
664
|
return useQuery({
|
|
574
665
|
queryKey: ["posts", page, limit],
|
|
575
666
|
queryFn: async () => {
|
|
576
|
-
const response = await
|
|
667
|
+
const response = await rpc.posts.$get({
|
|
577
668
|
query: { page: String(page), limit: String(limit) },
|
|
578
669
|
});
|
|
579
670
|
return parseResponse(response, paginatedPostListSchema);
|
|
@@ -651,26 +742,45 @@ export const adminMiddleware = createMiddleware(async (c, next) => {
|
|
|
651
742
|
})
|
|
652
743
|
```
|
|
653
744
|
|
|
654
|
-
###
|
|
745
|
+
### ミドルウェアの適用(ドメインベースRPC分割)
|
|
746
|
+
|
|
747
|
+
ドメインベースRPC分割では、各ドメインのroute.tsが独立したHonoアプリを持つため、ミドルウェアはドメインごとに個別に適用します。
|
|
655
748
|
|
|
656
749
|
```typescript
|
|
657
|
-
|
|
658
|
-
import {
|
|
750
|
+
// 認証不要のドメイン: apps/web/app/api/rpc/auth/[[...route]]/route.ts
|
|
751
|
+
import { authRoutes } from "@web/server/presentation/routes/authRoutes";
|
|
752
|
+
import { Hono } from "hono";
|
|
753
|
+
import { handle } from "hono/vercel";
|
|
659
754
|
|
|
660
|
-
const app = new Hono()
|
|
755
|
+
const app = new Hono().basePath("/api/rpc/auth");
|
|
756
|
+
const routes = app.route("/", authRoutes);
|
|
757
|
+
export type AuthAppType = typeof routes;
|
|
661
758
|
|
|
662
|
-
|
|
663
|
-
|
|
759
|
+
export const GET = handle(app);
|
|
760
|
+
export const POST = handle(app);
|
|
761
|
+
```
|
|
664
762
|
|
|
665
|
-
|
|
666
|
-
app
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
763
|
+
```typescript
|
|
764
|
+
// 認証必要のドメイン: apps/web/app/api/rpc/posts/[[...route]]/route.ts
|
|
765
|
+
import { postRoutes } from "@web/server/presentation/routes/postRoutes";
|
|
766
|
+
import { authMiddleware } from "@/server/middleware/auth";
|
|
767
|
+
import { Hono } from "hono";
|
|
768
|
+
import { handle } from "hono/vercel";
|
|
769
|
+
|
|
770
|
+
const app = new Hono()
|
|
771
|
+
.basePath("/api/rpc/posts")
|
|
772
|
+
.use("/*", authMiddleware); // ← ドメインroute.tsで認証ミドルウェアを適用
|
|
773
|
+
const routes = app.route("/", postRoutes);
|
|
774
|
+
export type PostsAppType = typeof routes;
|
|
775
|
+
|
|
776
|
+
export const GET = handle(app);
|
|
777
|
+
export const POST = handle(app);
|
|
778
|
+
export const PUT = handle(app);
|
|
779
|
+
export const DELETE = handle(app);
|
|
780
|
+
export const PATCH = handle(app);
|
|
671
781
|
```
|
|
672
782
|
|
|
673
|
-
**⚠️ 重要**:
|
|
783
|
+
**⚠️ 重要**: サブルート(postRoutes等)内で`.use()`を使用するとHono RPC型推論が壊れます。ミドルウェアは必ずドメインroute.tsのアプリ側で適用してください。
|
|
674
784
|
詳細は[ミドルウェアと型推論の注意点](#ミドルウェアと型推論の注意点)を参照してください。
|
|
675
785
|
|
|
676
786
|
---
|
|
@@ -789,24 +899,46 @@ app.post(
|
|
|
789
899
|
export default app
|
|
790
900
|
```
|
|
791
901
|
|
|
792
|
-
|
|
902
|
+
**エントリーポイント(ドメインroute.ts)**:
|
|
793
903
|
|
|
794
904
|
```typescript
|
|
795
|
-
// apps/web/
|
|
796
|
-
import {
|
|
797
|
-
import {
|
|
798
|
-
import {
|
|
905
|
+
// apps/web/app/api/rpc/posts/[[...route]]/route.ts
|
|
906
|
+
import { postRoutes } from "@web/server/presentation/routes/postRoutes";
|
|
907
|
+
import { authMiddleware } from "@/server/middleware/auth";
|
|
908
|
+
import { Hono } from "hono";
|
|
909
|
+
import { handle } from "hono/vercel";
|
|
799
910
|
|
|
800
|
-
const app = new Hono()
|
|
911
|
+
const app = new Hono()
|
|
912
|
+
.basePath("/api/rpc/posts")
|
|
913
|
+
.use("/*", authMiddleware);
|
|
914
|
+
const routes = app.route("/", postRoutes);
|
|
915
|
+
|
|
916
|
+
export type PostsAppType = typeof routes;
|
|
801
917
|
|
|
802
|
-
const
|
|
918
|
+
export const GET = handle(app);
|
|
919
|
+
export const POST = handle(app);
|
|
920
|
+
export const PUT = handle(app);
|
|
921
|
+
export const DELETE = handle(app);
|
|
922
|
+
export const PATCH = handle(app);
|
|
923
|
+
```
|
|
803
924
|
|
|
804
|
-
|
|
805
|
-
export const POST = handle(app)
|
|
806
|
-
export const PUT = handle(app)
|
|
807
|
-
export const DELETE = handle(app)
|
|
925
|
+
**RPCクライアント**:
|
|
808
926
|
|
|
809
|
-
|
|
927
|
+
> **Note**: 以下は複数ドメインを追加した場合の完成形の例です。現在のテンプレートでは `users` ドメインのみ実装されています。
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
// apps/web/src/lib/api/rpc.ts
|
|
931
|
+
import type { AuthAppType } from "@/app/api/rpc/auth/[[...route]]/route";
|
|
932
|
+
import type { PostsAppType } from "@/app/api/rpc/posts/[[...route]]/route";
|
|
933
|
+
import { hc } from "hono/client";
|
|
934
|
+
|
|
935
|
+
const authClient = hc<AuthAppType>("/");
|
|
936
|
+
const postsClient = hc<PostsAppType>("/");
|
|
937
|
+
|
|
938
|
+
export const rpc = {
|
|
939
|
+
auth: authClient.api.rpc.auth,
|
|
940
|
+
posts: postsClient.api.rpc.posts,
|
|
941
|
+
} as const;
|
|
810
942
|
```
|
|
811
943
|
|
|
812
944
|
**フロントエンドでの使用**:
|
|
@@ -194,7 +194,7 @@ graph TD
|
|
|
194
194
|
|
|
195
195
|
#### 📕 Presentation層(API Routes)
|
|
196
196
|
|
|
197
|
-
**配置**: `apps/web/src/app/api/`, `apps/admin/src/app/api/`
|
|
197
|
+
**配置**: `apps/web/src/app/api/rpc/{domain}/`, `apps/admin/src/app/api/rpc/{domain}/`
|
|
198
198
|
|
|
199
199
|
**責務**:
|
|
200
200
|
- HTTPリクエスト/レスポンスの処理
|
|
@@ -204,28 +204,24 @@ graph TD
|
|
|
204
204
|
|
|
205
205
|
**技術**: Hono、zValidator、Zod
|
|
206
206
|
|
|
207
|
+
**エントリーポイント**: `/api/rpc/{domain}/[[...route]]/route.ts` (ドメインベースRPC分割)
|
|
208
|
+
|
|
207
209
|
**実装例**:
|
|
208
210
|
```typescript
|
|
209
|
-
// apps/web/src/app/api/posts/route.ts
|
|
211
|
+
// apps/web/src/app/api/rpc/posts/[[...route]]/route.ts
|
|
212
|
+
import { postRoutes } from "@web/server/presentation/routes/postRoutes"
|
|
210
213
|
import { Hono } from "hono"
|
|
211
|
-
import {
|
|
212
|
-
import { postSchema } from "@repo/server-core/domain/validators/post"
|
|
213
|
-
import { postUseCases } from "@/application/use-cases/PostUseCases" // アプリ内のApplication層
|
|
214
|
+
import { handle } from "hono/vercel"
|
|
214
215
|
|
|
215
216
|
const app = new Hono()
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (!result.isSuccess) {
|
|
221
|
-
return c.json({ error: result.error.message }, result.error.statusCode)
|
|
222
|
-
}
|
|
217
|
+
.basePath("/api/rpc/posts")
|
|
218
|
+
.use("/*", authMiddleware)
|
|
219
|
+
const routes = app.route("/", postRoutes)
|
|
223
220
|
|
|
224
|
-
|
|
225
|
-
})
|
|
221
|
+
export type PostsAppType = typeof routes
|
|
226
222
|
|
|
227
|
-
export const GET = app
|
|
228
|
-
export const POST = app
|
|
223
|
+
export const GET = handle(app)
|
|
224
|
+
export const POST = handle(app)
|
|
229
225
|
```
|
|
230
226
|
|
|
231
227
|
---
|
|
@@ -60,10 +60,10 @@ apps/web/
|
|
|
60
60
|
│ │ │ │ └── page.tsx # 投稿詳細
|
|
61
61
|
│ │ │ └── profile/
|
|
62
62
|
│ │ │ └── page.tsx
|
|
63
|
-
│ │ ├── api/
|
|
64
|
-
│ │ │
|
|
65
|
-
│ │ │
|
|
66
|
-
│ │ │
|
|
63
|
+
│ │ ├── api/rpc/ # ドメインベースRPC
|
|
64
|
+
│ │ │ ├── users/[[...route]]/route.ts
|
|
65
|
+
│ │ │ ├── auth/[[...route]]/route.ts
|
|
66
|
+
│ │ │ └── posts/[[...route]]/route.ts
|
|
67
67
|
│ │ ├── layout.tsx # ルートレイアウト
|
|
68
68
|
│ │ └── page.tsx # トップページ
|
|
69
69
|
│ ├── components/ # UIコンポーネント
|
|
@@ -84,7 +84,7 @@ apps/web/
|
|
|
84
84
|
│ │ └── RegisterForm.tsx
|
|
85
85
|
│ ├── lib/ # ユーティリティ
|
|
86
86
|
│ │ ├── api/
|
|
87
|
-
│ │ │ ├──
|
|
87
|
+
│ │ │ ├── rpc.ts # Hono RPCクライアント設定
|
|
88
88
|
│ │ │ └── parse-response.ts # レスポンスパース&バリデーション
|
|
89
89
|
│ │ ├── query-client.ts # Tanstack Query設定
|
|
90
90
|
│ │ └── utils.ts # 共通ユーティリティ
|
|
@@ -116,10 +116,9 @@ apps/admin/
|
|
|
116
116
|
│ │ │ │ │ └── page.tsx
|
|
117
117
|
│ │ │ │ └── analytics/
|
|
118
118
|
│ │ │ │ └── page.tsx
|
|
119
|
-
│ │ ├── api/
|
|
120
|
-
│ │ │
|
|
121
|
-
│ │ │
|
|
122
|
-
│ │ │ └── route.ts
|
|
119
|
+
│ │ ├── api/rpc/ # ドメインベースRPC
|
|
120
|
+
│ │ │ ├── users/[[...route]]/route.ts
|
|
121
|
+
│ │ │ └── posts/[[...route]]/route.ts
|
|
123
122
|
│ │ ├── layout.tsx
|
|
124
123
|
│ │ └── page.tsx
|
|
125
124
|
│ ├── components/
|
|
@@ -198,27 +197,39 @@ export const paginatedUserListSchema = z.object({
|
|
|
198
197
|
|
|
199
198
|
### セットアップ
|
|
200
199
|
|
|
201
|
-
**
|
|
200
|
+
**RPCクライアントの初期化**:
|
|
202
201
|
|
|
203
|
-
|
|
204
|
-
// apps/web/src/lib/api/client.ts
|
|
205
|
-
import { hc } from 'hono/client'
|
|
206
|
-
import type { AppType } from '@/app/api/rpc/[[...route]]/route'
|
|
202
|
+
> **Note**: 以下は複数ドメインを追加した場合の完成形の例です。現在のテンプレートでは `users` ドメインのみ実装されています。
|
|
207
203
|
|
|
208
|
-
|
|
204
|
+
```typescript
|
|
205
|
+
// apps/web/src/lib/api/rpc.ts
|
|
206
|
+
import type { UsersAppType } from "@/app/api/rpc/users/[[...route]]/route";
|
|
207
|
+
import type { AuthAppType } from "@/app/api/rpc/auth/[[...route]]/route";
|
|
208
|
+
import type { PostsAppType } from "@/app/api/rpc/posts/[[...route]]/route";
|
|
209
|
+
import { hc } from "hono/client";
|
|
210
|
+
|
|
211
|
+
const usersClient = hc<UsersAppType>("/");
|
|
212
|
+
const authClient = hc<AuthAppType>("/");
|
|
213
|
+
const postsClient = hc<PostsAppType>("/");
|
|
214
|
+
|
|
215
|
+
export const rpc = {
|
|
216
|
+
users: usersClient.api.rpc.users,
|
|
217
|
+
auth: authClient.api.rpc.auth,
|
|
218
|
+
posts: postsClient.api.rpc.posts,
|
|
219
|
+
} as const;
|
|
209
220
|
```
|
|
210
221
|
|
|
211
|
-
|
|
222
|
+
**型定義のエクスポート**(ドメインごとのルートファイル):
|
|
212
223
|
|
|
213
224
|
```typescript
|
|
214
|
-
// apps/web/src/app/api/rpc/[[...route]]/route.ts
|
|
225
|
+
// apps/web/src/app/api/rpc/users/[[...route]]/route.ts
|
|
215
226
|
import { Hono } from 'hono'
|
|
216
227
|
import { handle } from 'hono/vercel'
|
|
217
228
|
import { userRoutes } from '@web/server/presentation/routes/userRoutes'
|
|
218
229
|
|
|
219
|
-
const app = new Hono().basePath('/api/rpc')
|
|
230
|
+
const app = new Hono().basePath('/api/rpc/users')
|
|
220
231
|
|
|
221
|
-
const routes = app.route('/
|
|
232
|
+
const routes = app.route('/', userRoutes)
|
|
222
233
|
|
|
223
234
|
export const GET = handle(app)
|
|
224
235
|
export const POST = handle(app)
|
|
@@ -226,7 +237,7 @@ export const PUT = handle(app)
|
|
|
226
237
|
export const DELETE = handle(app)
|
|
227
238
|
|
|
228
239
|
// 型のエクスポート(フロントエンドで使用)
|
|
229
|
-
export type
|
|
240
|
+
export type UsersAppType = typeof routes
|
|
230
241
|
```
|
|
231
242
|
|
|
232
243
|
### API呼び出しパターン
|
|
@@ -235,7 +246,7 @@ export type AppType = typeof routes
|
|
|
235
246
|
|
|
236
247
|
```typescript
|
|
237
248
|
// ユーザー一覧取得
|
|
238
|
-
const response = await
|
|
249
|
+
const response = await rpc.users.$get({
|
|
239
250
|
query: { page: '1', limit: '10' }
|
|
240
251
|
})
|
|
241
252
|
const data = await response.json() // 型推論: { users: User[], total: number }
|
|
@@ -245,7 +256,7 @@ const data = await response.json() // 型推論: { users: User[], total: number
|
|
|
245
256
|
|
|
246
257
|
```typescript
|
|
247
258
|
// ユーザー作成
|
|
248
|
-
const response = await
|
|
259
|
+
const response = await rpc.users.$post({
|
|
249
260
|
json: { email: 'user@example.com', name: 'User Name' }
|
|
250
261
|
})
|
|
251
262
|
const data = await response.json() // 型推論: { user: User }
|
|
@@ -255,7 +266,7 @@ const data = await response.json() // 型推論: { user: User }
|
|
|
255
266
|
|
|
256
267
|
```typescript
|
|
257
268
|
// ユーザー詳細取得
|
|
258
|
-
const response = await
|
|
269
|
+
const response = await rpc.users[':id'].$get({
|
|
259
270
|
param: { id: '123' }
|
|
260
271
|
})
|
|
261
272
|
const data = await response.json() // 型推論: { user: User }
|
|
@@ -265,7 +276,7 @@ const data = await response.json() // 型推論: { user: User }
|
|
|
265
276
|
|
|
266
277
|
```typescript
|
|
267
278
|
// ユーザー更新
|
|
268
|
-
const response = await
|
|
279
|
+
const response = await rpc.users[':id'].$put({
|
|
269
280
|
param: { id: '123' },
|
|
270
281
|
json: { name: 'Updated Name' }
|
|
271
282
|
})
|
|
@@ -276,7 +287,7 @@ const data = await response.json() // 型推論: { user: User }
|
|
|
276
287
|
|
|
277
288
|
```typescript
|
|
278
289
|
// ユーザー削除
|
|
279
|
-
const response = await
|
|
290
|
+
const response = await rpc.users[':id'].$delete({
|
|
280
291
|
param: { id: '123' }
|
|
281
292
|
})
|
|
282
293
|
const data = await response.json() // 型推論: { success: true }
|
|
@@ -344,13 +355,13 @@ export async function parseResponse<T>(
|
|
|
344
355
|
import { useQuery } from "@tanstack/react-query";
|
|
345
356
|
import { parseResponse } from "@/lib/api/parse-response";
|
|
346
357
|
import { paginatedUserListSchema } from "@/shared/schemas/user";
|
|
347
|
-
import {
|
|
358
|
+
import { rpc } from "@/lib/api/rpc";
|
|
348
359
|
|
|
349
360
|
export function useUsers(filters: UserFilters = {}) {
|
|
350
361
|
return useQuery({
|
|
351
362
|
queryKey: ["users", filters],
|
|
352
363
|
queryFn: async () => {
|
|
353
|
-
const response = await
|
|
364
|
+
const response = await rpc.users.$get({
|
|
354
365
|
query: { page: String(filters.page || 1), limit: String(filters.limit || 10) },
|
|
355
366
|
});
|
|
356
367
|
return parseResponse(response, paginatedUserListSchema);
|
|
@@ -417,11 +428,11 @@ try {
|
|
|
417
428
|
```typescript
|
|
418
429
|
// app/posts/page.tsx (Server Component - デフォルト)
|
|
419
430
|
import { PostList } from '@/components/features/posts/PostList'
|
|
420
|
-
import {
|
|
431
|
+
import { rpc } from '@/lib/api/rpc'
|
|
421
432
|
|
|
422
433
|
export default async function PostsPage() {
|
|
423
434
|
// サーバー側でデータフェッチ
|
|
424
|
-
const response = await
|
|
435
|
+
const response = await rpc.posts.$get({
|
|
425
436
|
query: { page: '1', limit: '10' }
|
|
426
437
|
})
|
|
427
438
|
const data = await response.json()
|
|
@@ -546,11 +557,11 @@ export default function PostsPage() {
|
|
|
546
557
|
import { Header } from '@/components/features/Header'
|
|
547
558
|
import { Sidebar } from '@/components/features/Sidebar'
|
|
548
559
|
import { PostListContainer } from '@/components/features/posts/PostListContainer'
|
|
549
|
-
import {
|
|
560
|
+
import { rpc } from '@/lib/api/rpc'
|
|
550
561
|
|
|
551
562
|
export default async function PostsPage() {
|
|
552
563
|
// サーバー側でデータフェッチ
|
|
553
|
-
const response = await
|
|
564
|
+
const response = await rpc.posts.$get()
|
|
554
565
|
const data = await response.json()
|
|
555
566
|
|
|
556
567
|
return (
|
|
@@ -596,11 +607,11 @@ export function PostListContainer({ initialData }) {
|
|
|
596
607
|
```typescript
|
|
597
608
|
// app/posts/[id]/page.tsx (Server Component)
|
|
598
609
|
import { PostDetail } from '@/components/features/posts/PostDetail'
|
|
599
|
-
import {
|
|
610
|
+
import { rpc } from '@/lib/api/rpc'
|
|
600
611
|
|
|
601
612
|
export default async function PostDetailPage({ params }: { params: { id: string } }) {
|
|
602
613
|
// サーバー側でデータフェッチ
|
|
603
|
-
const response = await
|
|
614
|
+
const response = await rpc.posts[':id'].$get({
|
|
604
615
|
param: { id: params.id }
|
|
605
616
|
})
|
|
606
617
|
const { post } = await response.json()
|
|
@@ -797,13 +808,13 @@ export default function RootLayout({ children }) {
|
|
|
797
808
|
|
|
798
809
|
```typescript
|
|
799
810
|
import { useQuery } from '@tanstack/react-query'
|
|
800
|
-
import {
|
|
811
|
+
import { rpc } from '@/lib/api/rpc'
|
|
801
812
|
|
|
802
813
|
export function usePostList(page: number, limit: number) {
|
|
803
814
|
return useQuery({
|
|
804
815
|
queryKey: ['posts', page, limit], // キャッシュキー
|
|
805
816
|
queryFn: async () => {
|
|
806
|
-
const response = await
|
|
817
|
+
const response = await rpc.posts.$get({
|
|
807
818
|
query: { page: String(page), limit: String(limit) }
|
|
808
819
|
})
|
|
809
820
|
if (!response.ok) {
|
|
@@ -841,7 +852,7 @@ export function PostList() {
|
|
|
841
852
|
|
|
842
853
|
```typescript
|
|
843
854
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
844
|
-
import {
|
|
855
|
+
import { rpc } from '@/lib/api/rpc'
|
|
845
856
|
import type { CreatePostInput } from '@repo/server-core/domain/validators/post'
|
|
846
857
|
|
|
847
858
|
export function useCreatePost() {
|
|
@@ -849,7 +860,7 @@ export function useCreatePost() {
|
|
|
849
860
|
|
|
850
861
|
return useMutation({
|
|
851
862
|
mutationFn: async (data: CreatePostInput) => {
|
|
852
|
-
const response = await
|
|
863
|
+
const response = await rpc.posts.$post({ json: data })
|
|
853
864
|
if (!response.ok) {
|
|
854
865
|
throw new Error('Failed to create post')
|
|
855
866
|
}
|
|
@@ -1111,7 +1122,7 @@ export function PostCard({ post }: PostCardProps) {
|
|
|
1111
1122
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
1112
1123
|
import { parseResponse } from '@/lib/api/parse-response'
|
|
1113
1124
|
import { paginatedPostListSchema } from '@/shared/schemas/post'
|
|
1114
|
-
import {
|
|
1125
|
+
import { rpc } from '@/lib/api/rpc'
|
|
1115
1126
|
import type { CreatePostInput, UpdatePostInput } from '@repo/server-core/domain/validators/post'
|
|
1116
1127
|
|
|
1117
1128
|
// 投稿一覧取得
|
|
@@ -1119,7 +1130,7 @@ export function usePostList(page: number, limit: number) {
|
|
|
1119
1130
|
return useQuery({
|
|
1120
1131
|
queryKey: ['posts', page, limit],
|
|
1121
1132
|
queryFn: async () => {
|
|
1122
|
-
const response = await
|
|
1133
|
+
const response = await rpc.posts.$get({
|
|
1123
1134
|
query: { page: String(page), limit: String(limit) }
|
|
1124
1135
|
})
|
|
1125
1136
|
return parseResponse(response, paginatedPostListSchema)
|
|
@@ -1132,7 +1143,7 @@ export function usePost(id: string) {
|
|
|
1132
1143
|
return useQuery({
|
|
1133
1144
|
queryKey: ['posts', id],
|
|
1134
1145
|
queryFn: async () => {
|
|
1135
|
-
const response = await
|
|
1146
|
+
const response = await rpc.posts[':id'].$get({ param: { id } })
|
|
1136
1147
|
return parseResponse(response, postSchema)
|
|
1137
1148
|
},
|
|
1138
1149
|
})
|
|
@@ -1144,7 +1155,7 @@ export function useCreatePost() {
|
|
|
1144
1155
|
|
|
1145
1156
|
return useMutation({
|
|
1146
1157
|
mutationFn: async (data: CreatePostInput) => {
|
|
1147
|
-
const response = await
|
|
1158
|
+
const response = await rpc.posts.$post({ json: data })
|
|
1148
1159
|
return parseResponse(response, postSchema)
|
|
1149
1160
|
},
|
|
1150
1161
|
onSuccess: () => {
|
|
@@ -1159,7 +1170,7 @@ export function useUpdatePost(id: string) {
|
|
|
1159
1170
|
|
|
1160
1171
|
return useMutation({
|
|
1161
1172
|
mutationFn: async (data: UpdatePostInput) => {
|
|
1162
|
-
const response = await
|
|
1173
|
+
const response = await rpc.posts[':id'].$put({
|
|
1163
1174
|
param: { id },
|
|
1164
1175
|
json: data
|
|
1165
1176
|
})
|
|
@@ -1178,7 +1189,7 @@ export function useDeletePost() {
|
|
|
1178
1189
|
|
|
1179
1190
|
return useMutation({
|
|
1180
1191
|
mutationFn: async (id: string) => {
|
|
1181
|
-
const response = await
|
|
1192
|
+
const response = await rpc.posts[':id'].$delete({ param: { id } })
|
|
1182
1193
|
return parseResponse(response, deleteResponseSchema)
|
|
1183
1194
|
},
|
|
1184
1195
|
onSuccess: () => {
|
|
@@ -1332,12 +1343,12 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|
|
1332
1343
|
// ✅ page.tsxはServer Component('use client'なし)
|
|
1333
1344
|
import { PostListContainer } from '@/components/features/posts/PostListContainer'
|
|
1334
1345
|
import { Button } from '@/components/ui/button'
|
|
1335
|
-
import {
|
|
1346
|
+
import { rpc } from '@/lib/api/rpc'
|
|
1336
1347
|
import Link from 'next/link'
|
|
1337
1348
|
|
|
1338
1349
|
export default async function PostsPage() {
|
|
1339
1350
|
// サーバー側でデータフェッチ
|
|
1340
|
-
const response = await
|
|
1351
|
+
const response = await rpc.posts.$get({
|
|
1341
1352
|
query: { page: '1', limit: '10' }
|
|
1342
1353
|
})
|
|
1343
1354
|
const data = await response.json()
|
|
@@ -1402,11 +1413,11 @@ export default function PostNewPage() {
|
|
|
1402
1413
|
```typescript
|
|
1403
1414
|
// ✅ page.tsxはServer Component('use client'なし)
|
|
1404
1415
|
import { PostDetail } from '@/components/features/posts/PostDetail'
|
|
1405
|
-
import {
|
|
1416
|
+
import { rpc } from '@/lib/api/rpc'
|
|
1406
1417
|
|
|
1407
1418
|
export default async function PostDetailPage({ params }: { params: { id: string } }) {
|
|
1408
1419
|
// サーバー側でデータフェッチ
|
|
1409
|
-
const response = await
|
|
1420
|
+
const response = await rpc.posts[':id'].$get({
|
|
1410
1421
|
param: { id: params.id }
|
|
1411
1422
|
})
|
|
1412
1423
|
const { post } = await response.json()
|
|
@@ -1453,7 +1464,7 @@ export function usePostList(page: number, limit: number) {
|
|
|
1453
1464
|
return useQuery({
|
|
1454
1465
|
queryKey: ['posts', page, limit],
|
|
1455
1466
|
queryFn: async () => {
|
|
1456
|
-
const response = await
|
|
1467
|
+
const response = await rpc.posts.$get({
|
|
1457
1468
|
query: { page: String(page), limit: String(limit) }
|
|
1458
1469
|
})
|
|
1459
1470
|
|
|
@@ -1531,7 +1542,7 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
1531
1542
|
// ✅ page.tsxはServer Component('use client'なし)
|
|
1532
1543
|
import { PostListWithPagination } from '@/components/features/posts/PostListWithPagination'
|
|
1533
1544
|
import { Button } from '@/components/ui/button'
|
|
1534
|
-
import {
|
|
1545
|
+
import { rpc } from '@/lib/api/rpc'
|
|
1535
1546
|
import Link from 'next/link'
|
|
1536
1547
|
|
|
1537
1548
|
interface PostsPageProps {
|
|
@@ -1543,7 +1554,7 @@ export default async function PostsPage({ searchParams }: PostsPageProps) {
|
|
|
1543
1554
|
const limit = 10
|
|
1544
1555
|
|
|
1545
1556
|
// サーバー側でデータフェッチ
|
|
1546
|
-
const response = await
|
|
1557
|
+
const response = await rpc.posts.$get({
|
|
1547
1558
|
query: { page: String(page), limit: String(limit) }
|
|
1548
1559
|
})
|
|
1549
1560
|
const data = await response.json()
|
|
@@ -71,6 +71,8 @@ APIエンドポイントやアプリケーション機能の実装レビュー
|
|
|
71
71
|
- Zodスキーマによるバリデーションが全エンドポイントで実装されているか
|
|
72
72
|
- **🔗 Honoのメソッドチェーン形式が守られているか** - `new Hono().get().post().put().delete()`の形式で実装すること。個別呼び出し形式(`app.get(); app.post();`)では`typeof app`による型推論が損なわれ、Hono Clientでの型安全性が失われる(詳細: [API開発ガイド - メソッドチェーンパターン](./development/api-development.md#メソッドチェーンパターン))
|
|
73
73
|
- **🔗 サブルート内で`.use()`を使っていないか** - ミドルウェアはメインアプリ側で適用すること。サブルート内で`.use()`を使うと型が`ClientRequest<{}>`になり型推論が壊れる(詳細: [API開発ガイド - ミドルウェアと型推論](./development/api-development.md#ミドルウェアと型推論の注意点))
|
|
74
|
+
- **🔗 ドメインベースRPC分割が守られているか** - 各ドメインごとに `/api/rpc/{domain}/[[...route]]/route.ts` のエントリーポイントを作成すること
|
|
75
|
+
- **🔗 単一catch-allに全ルートを集約していないか** - 1つの `[[...route]]/route.ts` に複数ドメインのルートをまとめて登録してはならない。ドメインごとにエントリーポイントを分割すること
|
|
74
76
|
- エラーハンドリングがApplicationErrorで統一されているか
|
|
75
77
|
- loggerが使用され、console.logが使われていないか
|
|
76
78
|
- **相対パスの使用禁止** - import文、require文、ファイルパス指定で`../`や`./`などの相対パスが使用されていないか。必ずアプリ固有エイリアス(`@web/*`、`@admin/*`等)またはパッケージ名(`@repo/server-core`等)を使用すること
|
|
@@ -82,7 +84,8 @@ APIエンドポイントやアプリケーション機能の実装レビュー
|
|
|
82
84
|
- [ ] **APIクライアント実装ガイドの実装ルール遵守**
|
|
83
85
|
- Tanstack QueryとHonoクライアントを使用した型安全な実装になっているか
|
|
84
86
|
- Zodスキーマによるレスポンス検証が実装されているか
|
|
85
|
-
-
|
|
87
|
+
- `rpc`オブジェクト(ドメインごとのHono Client)を使用しているか
|
|
88
|
+
- 旧`apiClient`パターン(単一クライアント)を使用していないか(使用禁止)
|
|
86
89
|
- useMutationでのキャッシュ無効化が適切に実装されているか
|
|
87
90
|
- エラーハンドリングが実装されているか
|
|
88
91
|
- 型定義の整合性が保たれているか(バックエンドとフロントエンド間)
|
|
@@ -425,7 +425,7 @@ describe('UserRepository - データベースエラー', () => {
|
|
|
425
425
|
|
|
426
426
|
```typescript
|
|
427
427
|
// apps/web/__tests__/integration/post-api.test.ts
|
|
428
|
-
import {
|
|
428
|
+
import { rpc } from '@/lib/api/rpc'
|
|
429
429
|
import { prisma } from '@repo/server-core/infrastructure/database/client'
|
|
430
430
|
|
|
431
431
|
describe('Post API統合テスト', () => {
|
|
@@ -454,7 +454,7 @@ describe('Post API統合テスト', () => {
|
|
|
454
454
|
})
|
|
455
455
|
|
|
456
456
|
// When: API呼び出し
|
|
457
|
-
const response = await
|
|
457
|
+
const response = await rpc.posts.$get({
|
|
458
458
|
query: { page: '1', limit: '10' }
|
|
459
459
|
})
|
|
460
460
|
|
|
@@ -475,7 +475,7 @@ describe('Post API統合テスト', () => {
|
|
|
475
475
|
})
|
|
476
476
|
|
|
477
477
|
// When: 投稿作成APIを呼び出し
|
|
478
|
-
const response = await
|
|
478
|
+
const response = await rpc.posts.$post({
|
|
479
479
|
json: {
|
|
480
480
|
title: 'New Post',
|
|
481
481
|
content: 'Content',
|