@einja/dev-cli 0.1.12 → 0.1.14

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.
@@ -43,52 +43,58 @@ Tanstack Query、React Hook Form、Hono Clientを活用した型安全で保守
43
43
 
44
44
  ```
45
45
  apps/web/
46
- ├── app/ # Next.js App Router
47
- │ ├── (auth)/ # ルートグループ: 認証関連
48
- │ │ ├── login/
49
- │ │ │ └── page.tsx
50
- │ │ └── register/
51
- │ │ └── page.tsx
52
- ├── (dashboard)/ # ルートグループ: ダッシュボード
53
- │ │ ├── posts/
54
- │ │ │ ├── page.tsx # 投稿一覧
55
- │ │ │ ├── new/
56
- │ │ │ │ └── page.tsx # 投稿作成
57
- │ │ │ └── [id]/
58
- │ │ │ └── page.tsx # 投稿詳細
59
- │ │ └── profile/
60
- │ │ └── page.tsx
61
- ├── api/ # API Routes
62
- │ │ └── [[...route]]/
63
- │ │ └── route.ts # Honoエントリーポイント
64
- ├── layout.tsx # ルートレイアウト
65
- │ └── page.tsx # トップページ
66
- ├── components/ # UIコンポーネント
67
- ├── ui/ # 基本コンポーネント
68
- ├── button.tsx
69
- │ │ ├── card.tsx
70
- │ │ ├── input.tsx
71
- │ │ ├── modal.tsx
72
- │ │ └── ...
73
- └── features/ # 機能別コンポーネント
74
- ├── posts/
75
- ├── PostList.tsx
76
- ├── PostCard.tsx
77
- │ │ ├── PostCreateForm.tsx
78
- │ │ └── PostDetail.tsx
79
- └── auth/
80
- ├── LoginForm.tsx
81
- └── RegisterForm.tsx
82
- ├── lib/ # ユーティリティ
83
- ├── api-client.ts # Hono Client設定
84
- │ ├── query-client.ts # Tanstack Query設定
85
- └── utils.ts # 共通ユーティリティ
86
- ├── hooks/ # カスタムフック
87
- ├── use-posts.ts # Postデータフック
88
- └── use-auth.ts # 認証フック
46
+ ├── src/
47
+ │ ├── app/ # Next.js App Router
48
+ │ │ ├── (auth)/ # ルートグループ: 認証関連
49
+ │ │ │ ├── login/
50
+ │ │ │ │ └── page.tsx
51
+ │ │└── register/
52
+ │ │ └── page.tsx
53
+ │ │ ├── (dashboard)/ # ルートグループ: ダッシュボード
54
+ │ │ │ ├── posts/
55
+ │ │ │ ├── page.tsx # 投稿一覧
56
+ │ │ │ │ ├── new/
57
+ │ │ │ │ │ └── page.tsx # 投稿作成
58
+ │ │ │└── [id]/
59
+ │ │ │ │ └── page.tsx # 投稿詳細
60
+ │ │└── profile/
61
+ │ │ └── page.tsx
62
+ │ │ ├── api/ # API Routes
63
+ │ │└── rpc/
64
+ │ │ └── [[...route]]/
65
+ │ │ └── route.ts # Honoエントリーポイント
66
+ │ │ ├── layout.tsx # ルートレイアウト
67
+ │ └── page.tsx # トップページ
68
+ │ ├── components/ # UIコンポーネント
69
+ │ │ ├── ui/ # 基本コンポーネント
70
+ │ │ ├── button.tsx
71
+ │ │ ├── card.tsx
72
+ │ │ │ ├── input.tsx
73
+ │ │ ├── modal.tsx
74
+ │ │ └── ...
75
+ └── features/ # 機能別コンポーネント
76
+ ├── posts/
77
+ │ ├── PostList.tsx
78
+ ├── PostCard.tsx
79
+ │ ├── PostCreateForm.tsx
80
+ │ │ └── PostDetail.tsx
81
+ └── auth/
82
+ │ │ ├── LoginForm.tsx
83
+ │ └── RegisterForm.tsx
84
+ │ ├── lib/ # ユーティリティ
85
+ │ ├── api/
86
+ │ │ │ ├── client.ts # Hono Client設定
87
+ │ │ └── parse-response.ts # レスポンスパース&バリデーション
88
+ │ ├── query-client.ts # Tanstack Query設定
89
+ │ │ └── utils.ts # 共通ユーティリティ
90
+ │ ├── hooks/ # カスタムフック
91
+ │ │ ├── api/ # API関連フック
92
+ │ │ │ └── use-posts.ts
93
+ │ │ └── use-auth.ts # 認証フック
94
+ │ └── shared/
95
+ │ └── schemas/ # レスポンススキーマ(フロント固有)
96
+ │ └── user.ts
89
97
  ├── public/ # 静的ファイル
90
- ├── styles/ # グローバルスタイル
91
- │ └── globals.css
92
98
  ├── next.config.js
93
99
  ├── package.json
94
100
  ├── tsconfig.json
@@ -99,29 +105,31 @@ apps/web/
99
105
 
100
106
  ```
101
107
  apps/admin/
102
- ├── app/
103
- │ ├── (protected)/ # ルートグループ: 認証必須
104
- │ │ ├── admin/
105
- │ │ │ ├── users/
106
- │ │ │ │ └── page.tsx
107
- │ │ │ ├── posts/
108
- │ │ │ │ └── page.tsx
109
- │ │ │ └── analytics/
110
- │ │ │ └── page.tsx
111
- ├── api/
112
- │ │ └── [[...route]]/
113
- │ │ └── route.ts
114
- ├── layout.tsx
115
- │ └── page.tsx
116
- ├── components/
117
- ├── ui/
118
- └── features/
119
- └── admin/
120
- ├── UserTable.tsx
121
- └── PostStatusManager.tsx
122
- ├── lib/
123
- ├── hooks/
124
- └── ...
108
+ ├── src/
109
+ │ ├── app/
110
+ │ │ ├── (protected)/ # ルートグループ: 認証必須
111
+ │ │ │ ├── admin/
112
+ │ │ │ │ ├── users/
113
+ │ │ │ │ │ └── page.tsx
114
+ │ │ │ │ ├── posts/
115
+ │ │ │ │ │ └── page.tsx
116
+ │ │ │└── analytics/
117
+ │ │ │ └── page.tsx
118
+ │ │ ├── api/
119
+ │ │└── rpc/
120
+ │ │ └── [[...route]]/
121
+ │ │ └── route.ts
122
+ │ │ ├── layout.tsx
123
+ │ └── page.tsx
124
+ ├── components/
125
+ │ ├── ui/
126
+ │ └── features/
127
+ └── admin/
128
+ │ │ ├── UserTable.tsx
129
+ │ │ └── PostStatusManager.tsx
130
+ │ ├── lib/
131
+ │ ├── hooks/
132
+ │ └── ...
125
133
  ```
126
134
 
127
135
  **設計ポイント**:
@@ -129,6 +137,45 @@ apps/admin/
129
137
  - **コロケーション**: 機能別にコンポーネント、フック、ユーティリティを配置
130
138
  - **共通コンポーネント**: ui/に再利用可能な基本コンポーネント、features/に機能別コンポーネント
131
139
 
140
+ ### スキーマ配置の設計方針
141
+
142
+ スキーマはリクエスト/レスポンスで配置場所を分離します。
143
+
144
+ | スキーマ種別 | 配置場所 | 用途 |
145
+ |-------------|---------|------|
146
+ | **リクエストスキーマ** | `@repo/server-core/domain/validators/` | APIリクエストのバリデーション(バックエンド) |
147
+ | **レスポンススキーマ** | `apps/web/src/shared/schemas/` | APIレスポンスの型検証(フロントエンド固有) |
148
+
149
+ **理由**:
150
+ - レスポンス形式はフロントエンドが消費するものなので、フロントエンド側で定義すべき
151
+ - apps間で異なるレスポンス形式を持つ可能性がある
152
+ - フロント固有のバリデーションルール(例: 日付フォーマット)を追加しやすい
153
+
154
+ **例**:
155
+
156
+ ```typescript
157
+ // リクエストスキーマ(バックエンド)
158
+ // @repo/server-core/domain/validators/user.ts
159
+ export const createUserSchema = z.object({
160
+ email: z.string().email(),
161
+ name: z.string().min(1).max(100),
162
+ })
163
+
164
+ // レスポンススキーマ(フロントエンド)
165
+ // apps/web/src/shared/schemas/user.ts
166
+ export const userSchema = z.object({
167
+ id: z.string(),
168
+ email: z.string(),
169
+ name: z.string(),
170
+ createdAt: z.string(), // ISO 8601文字列
171
+ })
172
+
173
+ export const paginatedUserListSchema = z.object({
174
+ users: z.array(userSchema),
175
+ total: z.number(),
176
+ })
177
+ ```
178
+
132
179
  ---
133
180
 
134
181
  ## 2. 技術スタック
@@ -153,24 +200,24 @@ apps/admin/
153
200
  **Hono Clientの初期化**:
154
201
 
155
202
  ```typescript
156
- // apps/web/lib/api-client.ts
203
+ // apps/web/src/lib/api/client.ts
157
204
  import { hc } from 'hono/client'
158
- import type { AppType } from '@/app/api/[[...route]]/route'
205
+ import type { AppType } from '@/app/api/rpc/[[...route]]/route'
159
206
 
160
- export const apiClient = hc<AppType>('/api')
207
+ export const apiClient = hc<AppType>('/')
161
208
  ```
162
209
 
163
210
  **型定義のエクスポート**:
164
211
 
165
212
  ```typescript
166
- // apps/web/app/api/[[...route]]/route.ts
213
+ // apps/web/src/app/api/rpc/[[...route]]/route.ts
167
214
  import { Hono } from 'hono'
168
215
  import { handle } from 'hono/vercel'
169
- import postRoutes from '@/server/routes/postRoutes'
216
+ import { userRoutes } from '@web/server/presentation/routes/userRoutes'
170
217
 
171
- const app = new Hono().basePath('/api')
218
+ const app = new Hono().basePath('/api/rpc')
172
219
 
173
- app.route('/', postRoutes)
220
+ const routes = app.route('/users', userRoutes)
174
221
 
175
222
  export const GET = handle(app)
176
223
  export const POST = handle(app)
@@ -178,7 +225,7 @@ export const PUT = handle(app)
178
225
  export const DELETE = handle(app)
179
226
 
180
227
  // 型のエクスポート(フロントエンドで使用)
181
- export type AppType = typeof app
228
+ export type AppType = typeof routes
182
229
  ```
183
230
 
184
231
  ### API呼び出しパターン
@@ -186,49 +233,49 @@ export type AppType = typeof app
186
233
  **GET リクエスト**:
187
234
 
188
235
  ```typescript
189
- // 投稿一覧取得
190
- const response = await apiClient.posts.$get({
236
+ // ユーザー一覧取得
237
+ const response = await apiClient.api.rpc.users.$get({
191
238
  query: { page: '1', limit: '10' }
192
239
  })
193
- const data = await response.json() // 型推論: { posts: Post[], total: number }
240
+ const data = await response.json() // 型推論: { users: User[], total: number }
194
241
  ```
195
242
 
196
243
  **POST リクエスト**:
197
244
 
198
245
  ```typescript
199
- // 投稿作成
200
- const response = await apiClient.posts.$post({
201
- json: { title: 'New Post', content: 'Content' }
246
+ // ユーザー作成
247
+ const response = await apiClient.api.rpc.users.$post({
248
+ json: { email: 'user@example.com', name: 'User Name' }
202
249
  })
203
- const data = await response.json() // 型推論: { post: Post }
250
+ const data = await response.json() // 型推論: { user: User }
204
251
  ```
205
252
 
206
253
  **GET リクエスト(パスパラメータ)**:
207
254
 
208
255
  ```typescript
209
- // 投稿詳細取得
210
- const response = await apiClient.posts[':id'].$get({
256
+ // ユーザー詳細取得
257
+ const response = await apiClient.api.rpc.users[':id'].$get({
211
258
  param: { id: '123' }
212
259
  })
213
- const data = await response.json() // 型推論: { post: Post }
260
+ const data = await response.json() // 型推論: { user: User }
214
261
  ```
215
262
 
216
263
  **PUT リクエスト**:
217
264
 
218
265
  ```typescript
219
- // 投稿更新
220
- const response = await apiClient.posts[':id'].$put({
266
+ // ユーザー更新
267
+ const response = await apiClient.api.rpc.users[':id'].$put({
221
268
  param: { id: '123' },
222
- json: { title: 'Updated Title' }
269
+ json: { name: 'Updated Name' }
223
270
  })
224
- const data = await response.json() // 型推論: { post: Post }
271
+ const data = await response.json() // 型推論: { user: User }
225
272
  ```
226
273
 
227
274
  **DELETE リクエスト**:
228
275
 
229
276
  ```typescript
230
- // 投稿削除
231
- const response = await apiClient.posts[':id'].$delete({
277
+ // ユーザー削除
278
+ const response = await apiClient.api.rpc.users[':id'].$delete({
232
279
  param: { id: '123' }
233
280
  })
234
281
  const data = await response.json() // 型推論: { success: true }
@@ -239,6 +286,100 @@ const data = await response.json() // 型推論: { success: true }
239
286
  - 型エラーでAPI仕様の不一致を早期発見
240
287
  - IDEの補完機能でAPI仕様を確認可能
241
288
 
289
+ ### APIレスポンスパース処理
290
+
291
+ `lib/api/parse-response.ts` は、APIレスポンスのパースとZodスキーマによるバリデーションを行います。
292
+
293
+ **parseResponse 関数**:
294
+
295
+ ```typescript
296
+ import type { z } from "zod";
297
+
298
+ export class ApiError extends Error {
299
+ constructor(
300
+ public readonly code: string,
301
+ message: string,
302
+ public readonly statusCode: number,
303
+ public readonly details?: Record<string, unknown>
304
+ ) {
305
+ super(message);
306
+ this.name = "ApiError";
307
+ }
308
+ }
309
+
310
+ export async function parseResponse<T>(
311
+ response: Response,
312
+ schema: z.ZodSchema<T>
313
+ ): Promise<T> {
314
+ if (!response.ok) {
315
+ const errorData = await response.json().catch(() => ({}));
316
+ throw new ApiError(
317
+ errorData.error?.code || "UNKNOWN_ERROR",
318
+ errorData.error?.message || "APIエラーが発生しました",
319
+ response.status,
320
+ errorData.error?.details
321
+ );
322
+ }
323
+
324
+ const data = await response.json();
325
+ const parsed = schema.safeParse(data);
326
+
327
+ if (!parsed.success) {
328
+ throw new ApiError(
329
+ "VALIDATION_ERROR",
330
+ "レスポンスの形式が不正です",
331
+ 500,
332
+ { zodError: parsed.error.flatten() }
333
+ );
334
+ }
335
+
336
+ return parsed.data;
337
+ }
338
+ ```
339
+
340
+ **カスタムフック内での使用**:
341
+
342
+ ```typescript
343
+ import { useQuery } from "@tanstack/react-query";
344
+ import { parseResponse } from "@/lib/api/parse-response";
345
+ import { paginatedUserListSchema } from "@/shared/schemas/user";
346
+ import { apiClient } from "@/lib/api/client";
347
+
348
+ export function useUsers(filters: UserFilters = {}) {
349
+ return useQuery({
350
+ queryKey: ["users", filters],
351
+ queryFn: async () => {
352
+ const response = await apiClient.api.rpc.users.$get({
353
+ query: { page: String(filters.page || 1), limit: String(filters.limit || 10) },
354
+ });
355
+ return parseResponse(response, paginatedUserListSchema);
356
+ },
357
+ });
358
+ }
359
+ ```
360
+
361
+ **エラーハンドリング**:
362
+
363
+ ```typescript
364
+ import { ApiError } from "@/lib/api/parse-response";
365
+
366
+ try {
367
+ const users = await parseResponse(response, paginatedUserListSchema);
368
+ } catch (error) {
369
+ if (error instanceof ApiError) {
370
+ console.error(`API Error [${error.code}]: ${error.message}`);
371
+ // error.statusCode, error.details も利用可能
372
+ }
373
+ throw error;
374
+ }
375
+ ```
376
+
377
+ **メリット**:
378
+ - レスポンス形式の型安全性を保証
379
+ - フロント固有のバリデーションルールを適用可能
380
+ - エラーハンドリングの一元化
381
+ - APIエラーとバリデーションエラーの明確な区別
382
+
242
383
  ---
243
384
 
244
385
  ## 4. Server ComponentとClient Component
@@ -958,12 +1099,18 @@ export function PostCard({ post }: PostCardProps) {
958
1099
  - 状態管理ロジックの再利用
959
1100
  - コンポーネントをシンプルに保つ
960
1101
 
1102
+ **フックの分類**:
1103
+ - **API関連フック**: `hooks/api/` に配置(例: `use-posts.ts`, `use-users.ts`)
1104
+ - **UI状態フック**: `hooks/` 直下に配置(例: `use-toast.ts`, `use-auth.ts`)
1105
+
961
1106
  **例**:
962
1107
 
963
1108
  ```typescript
964
- // hooks/use-posts.ts
1109
+ // hooks/api/use-posts.ts
965
1110
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
966
- import { apiClient } from '@/lib/api-client'
1111
+ import { parseResponse } from '@/lib/api/parse-response'
1112
+ import { paginatedPostListSchema } from '@/shared/schemas/post'
1113
+ import { apiClient } from '@/lib/api/client'
967
1114
  import type { CreatePostInput, UpdatePostInput } from '@repo/server-core/domain/validators/post'
968
1115
 
969
1116
  // 投稿一覧取得
@@ -971,10 +1118,10 @@ export function usePostList(page: number, limit: number) {
971
1118
  return useQuery({
972
1119
  queryKey: ['posts', page, limit],
973
1120
  queryFn: async () => {
974
- const response = await apiClient.posts.$get({
1121
+ const response = await apiClient.api.posts.$get({
975
1122
  query: { page: String(page), limit: String(limit) }
976
1123
  })
977
- return response.json()
1124
+ return parseResponse(response, paginatedPostListSchema)
978
1125
  },
979
1126
  })
980
1127
  }
@@ -984,8 +1131,8 @@ export function usePost(id: string) {
984
1131
  return useQuery({
985
1132
  queryKey: ['posts', id],
986
1133
  queryFn: async () => {
987
- const response = await apiClient.posts[':id'].$get({ param: { id } })
988
- return response.json()
1134
+ const response = await apiClient.api.posts[':id'].$get({ param: { id } })
1135
+ return parseResponse(response, postSchema)
989
1136
  },
990
1137
  })
991
1138
  }
@@ -996,8 +1143,8 @@ export function useCreatePost() {
996
1143
 
997
1144
  return useMutation({
998
1145
  mutationFn: async (data: CreatePostInput) => {
999
- const response = await apiClient.posts.$post({ json: data })
1000
- return response.json()
1146
+ const response = await apiClient.api.posts.$post({ json: data })
1147
+ return parseResponse(response, postSchema)
1001
1148
  },
1002
1149
  onSuccess: () => {
1003
1150
  queryClient.invalidateQueries({ queryKey: ['posts'] })
@@ -1011,11 +1158,11 @@ export function useUpdatePost(id: string) {
1011
1158
 
1012
1159
  return useMutation({
1013
1160
  mutationFn: async (data: UpdatePostInput) => {
1014
- const response = await apiClient.posts[':id'].$put({
1161
+ const response = await apiClient.api.posts[':id'].$put({
1015
1162
  param: { id },
1016
1163
  json: data
1017
1164
  })
1018
- return response.json()
1165
+ return parseResponse(response, postSchema)
1019
1166
  },
1020
1167
  onSuccess: () => {
1021
1168
  queryClient.invalidateQueries({ queryKey: ['posts', id] })
@@ -1030,8 +1177,8 @@ export function useDeletePost() {
1030
1177
 
1031
1178
  return useMutation({
1032
1179
  mutationFn: async (id: string) => {
1033
- const response = await apiClient.posts[':id'].$delete({ param: { id } })
1034
- return response.json()
1180
+ const response = await apiClient.api.posts[':id'].$delete({ param: { id } })
1181
+ return parseResponse(response, deleteResponseSchema)
1035
1182
  },
1036
1183
  onSuccess: () => {
1037
1184
  queryClient.invalidateQueries({ queryKey: ['posts'] })
File without changes
@@ -1,26 +0,0 @@
1
- <!-- @einja:seed:start id="decisions" -->
2
- # 意思決定記録(Decisions)
3
-
4
- このファイルには、プロジェクトにおける重要な意思決定(Why)を記録します。
5
-
6
- ## 運用ルール
7
-
8
- - **最大エントリ数**: 30件
9
- - **上限到達時**: 古いエントリを `archive/decisions-{年}.md` に移動
10
- - **エントリ形式**: 以下のテンプレートに従う
11
-
12
- ### エントリテンプレート
13
-
14
- ```markdown
15
- ## [YYYY-MM-DD] タイトル
16
- - **結論**: 1-2文で結論
17
- - **理由**: 簡潔な理由
18
- - **見直し条件**: この決定を見直すべき条件
19
- ```
20
-
21
- ---
22
-
23
- ## 記録
24
-
25
- <!-- ここに意思決定を追加 -->
26
- <!-- @einja:seed:end -->
@@ -1,28 +0,0 @@
1
- <!-- @einja:seed:start id="patterns" -->
2
- # 再利用パターン(Patterns)
3
-
4
- このファイルには、プロジェクトで発見・確立された再利用可能なパターン(How)を記録します。
5
-
6
- ## 運用ルール
7
-
8
- - **最大エントリ数**: 30件
9
- - **上限到達時**: 古いエントリを `archive/patterns-{年}.md` に移動
10
- - **エントリ形式**: 以下のテンプレートに従う
11
-
12
- ### エントリテンプレート
13
-
14
- ```markdown
15
- ## [YYYY-MM-DD] パターン名
16
- - **問題**: どのような問題を解決するか
17
- - **解法**: どのように解決するか
18
- - **適用条件**: このパターンを使うべき状況
19
- - **非適用条件**: このパターンを使うべきでない状況
20
- - **例**: 具体的なコード例やユースケース
21
- ```
22
-
23
- ---
24
-
25
- ## 記録
26
-
27
- <!-- ここにパターンを追加 -->
28
- <!-- @einja:seed:end -->