1. 概要 { #overview }
REST APIで複数のリソースをIDで取得する機能の実装パターンと、NestJS + Zodを使った実装方法を解説します。
1.1 解決したい課題 { #problem }
単一のAPIリクエストで複数のユーザー情報を取得したい場合の最適な実装方法を検討します。
1.2 実装要件 { #requirements }
- 最大件数のIDを一度に指定可能
- 既存APIとの互換性維持
- 開発者に優しいインターフェース
- 型安全性の確保
2. 実装パターンの比較 { #patterns }
2.1 主要な3パターン { #three-patterns }
カンマ区切りパターン(推奨)
GET /api/users?ids=1,2,3,4,5
- 採用企業: Twitter、Google、Amazon
- メリット: URL最短、実装シンプル
- デメリット: カンマエスケープが必要な場合あり
配列形式パターン
GET /api/users?ids[]=1&ids[]=2&ids[]=3
- 採用企業: PHP/Rails系フレームワーク
- メリット: 配列であることが明確
- デメリット: URL長い、エンコーディング問題
リピートパラメータパターン
GET /api/users?id=1&id=2&id=3
- 採用企業: 一部のJavaフレームワーク
- メリット: HTTP標準準拠
- デメリット: 最も長いURL、実装により挙動が異なる
3. NestJS + Zodでの実装 { #implementation }
3.1 Zodスキーマ定義 { #zod-schema }
// schemas/user-query.schema.ts
import { z } from 'zod';
export const listUsersQuerySchema = z.object({
ids: z
.string()
.regex(/^\d+(,\d+)*$/, 'IDはカンマ区切りの数値である必要があります')
.transform(value => value.split(',').map(Number))
.pipe(
z.array(z.number().int().positive())
.min(1, '最低1つのIDが必要です')
.max(100, '最大100つまでのIDを指定できます')
)
.optional(),
});
3.2 コントローラー実装 { #controller }
@Controller('users')
export class UsersController {
@Get()
async list(@Query() query: ListUsersDto): Promise<UserResponse[]> {
return this.usersService.list(query.ids);
}
}
3.3 サービス層での注意点 { #service-tips }
async list(ids?: number[]): Promise<UserResponse[]> {
const users = await this.prisma.user.findMany({
where: { id: { in: ids } },
});
// 重要: リクエストされたID順序を保持
const userMap = new Map(users.map(u => [u.id, u]));
return ids
.map(id => userMap.get(id))
.filter(Boolean);
}
4. ケーススタディ:実装前後の比較 { #case-study }
4.1 実装前の問題点 { #before }
既存の実装: 単一IDのみ対応
// N+1問題が発生
const users = await Promise.all(
userIds.map(id => fetch(`/api/users/${id}`))
);
発生していた問題:
- 10ユーザー取得に10回のAPIコール
- レスポンス時間: 平均800ms
- ネットワーク負荷: 高
4.2 実装後の改善 { #after }
新しい実装: 複数ID対応
// 1回のAPIコールで完結
const users = await fetch(`/api/users?ids=${userIds.join(',')}`);
想定改善結果:
- APIコール数: 10回 → 1回
- レスポンス時間: 800ms → 150ms
- コード行数: 5行 → 1行
5. エラーハンドリング { #error-handling }
5.1 バリデーションエラーの例 { #validation-errors }
// 無効なフォーマット
GET /api/users?ids=1,invalid,3
{
"statusCode": 400,
"message": "バリデーションエラー",
"errors": [{
"field": "ids",
"message": "IDはカンマ区切りの数値である必要があります"
}]
}
5.2 URL長制限への対応 { #url-limit }
// 100件超える場合はPOSTエンドポイントを提供
@Post('batch')
async findUsersBatch(@Body() body: { ids: number[] }) {
return this.usersService.list(body.ids);
}
6. フロントエンド実装例 { #frontend }
6.1 React Hookでの利用 { #react-hook }
function useUsers(ids: number[]) {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
if (ids.length === 0) return;
fetch(`/api/users?ids=${ids.join(',')}`)
.then(res => res.json())
.then(setUsers);
}, [ids.join(',')]);
return users;
}
7. 運用のベストプラクティス { #best-practices }
7.1 実装チェックリスト { #checklist }
- カンマ区切り形式の採用
- 最大件数制限
- ID順序の保持
- 明確なエラーメッセージ
- 大量データ用のPOSTエンドポイント
7.2 パフォーマンス最適化 { #performance }
// データベースインデックスの活用
CREATE INDEX idx_users_id ON users(id);
// バッチ処理での取得
const BATCH_SIZE = 50;
const batches = chunk(ids, BATCH_SIZE);
8. まとめ { #conclusion }
8.1 導入効果 { #results }
- 開発効率: N+1問題の解消により実装がシンプルに
- パフォーマンス: レスポンス時間大部改善
- 保守性: Zodによる型安全性でバグ削減
- 互換性: 業界標準パターンで他サービスとの連携容易
8.2 今後の拡張案 { #future }
- GraphQL移行の検討(より柔軟なクエリが必要な場合)
- キャッシュ戦略の実装(頻繁にアクセスされるIDセット)
- フィールド選択機能の追加(
fields=id,name,email
)
カンマ区切りパターンとZodの組み合わせにより、シンプルで堅牢なAPIを実現できました。特に複数リソースの一括取得は、現代のWebアプリケーションにおいて必須の機能です。