2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

REST APIで複数ID指定を実装する:NestJS + Zodによる実践メモ

Last updated at Posted at 2025-08-04

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アプリケーションにおいて必須の機能です。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?