mcpField
P
pagination

pagination

github.com/claude-dev-suite開発4638 回インストール2 日前に更新
リポジトリ

pagination について

API のページネーションを実装する開発者向けのリファレンスです。カーソル、オフセット、キーセット方式を比較し、フィルタリングやソート、運用上の注意点も扱います。

主な機能

  1. 01カーソル・オフセット・キーセット方式
  2. 02フィルタリングとソートの例
  3. 03ページサイズと入力検証
  4. 04本番運用チェックリスト

活用シーン

  1. 01大規模データの方式選定
  2. 02管理用エンドポイントへのページング追加
  3. 03ページネーションの境界条件確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill pagination

インストール後、Claude Code で/paginationを入力して読み込まれたか確認します。

詳細

ページネーション

カーソルベース(大規模なデータセットに推奨)

app.get('/api/products', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const cursor = req.query.cursor as string | undefined;

  const where: any = {};
  if (cursor) {
    where.id = { gt: cursor };
  }

  const items = await db.product.findMany({
    where,
    take: limit + 1, // Fetch one extra to check hasMore
    orderBy: { id: 'asc' },
  });

  const hasMore = items.length > limit;
  if (hasMore) items.pop();

  res.json({
    data: items,
    pagination: {
      hasMore,
      nextCursor: hasMore ? items[items.length - 1].id : null,
    },
  });
});

オフセットベース(シンプルで、小規模なデータセットに適しています)

app.get('/api/products', async (req, res) => {
  const page = Math.max(parseInt(req.query.page as string) || 1, 1);
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const offset = (page - 1) * limit;

  const [items, total] = await Promise.all([
    db.product.findMany({ skip: offset, take: limit, orderBy: { createdAt: 'desc' } }),
    db.product.count(),
  ]);

  res.json({
    data: items,
    pagination: {
      page, limit, total,
      totalPages: Math.ceil(total / limit),
      hasMore: offset + items.length < total,
    },
  });
});

フィルタリングとソート

app.get('/api/products', async (req, res) => {
  const { sort = 'createdAt', order = 'desc', category, minPrice, maxPrice, search } = req.query;

  const where: any = {};
  if (category) where.category = category;
  if (minPrice || maxPrice) {
    where.price = {};
    if (minPrice) where.price.gte = parseFloat(minPrice as string);
    if (maxPrice) where.price.lte = parseFloat(maxPrice as string);
  }
  if (search) where.name = { contains: search, mode: 'insensitive' };

  const items = await db.product.findMany({
    where,
    orderBy: { [sort as string]: order },
    take: limit,
    skip: offset,
  });

  res.json({ data: items, pagination: { /* ... */ } });
});

Spring Boot(Pageable)

@GetMapping("/products")
public Page<ProductDto> list(
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size,
    @RequestParam(defaultValue = "createdAt,desc") String[] sort) {

    Pageable pageable = PageRequest.of(page, Math.min(size, 100),
        Sort.by(Sort.Direction.fromString(sort[1]), sort[0]));
    return productRepo.findAll(pageable).map(mapper::toDto);
}

比較

戦略メリットデメリット最適な用途
オフセットシンプルで、ページを指定して移動可能大きなテーブルでは遅く、スキップ位置がずれる管理パネル、小規模なデータセット
カーソル高速で、データの挿入があっても安定ページ N を指定して移動できないフィード、無限スクロール、大規模なデータセット
キーセット高速で、スキップ位置がずれない複数列でのソートが複雑時系列データ、順序付きデータ

アンチパターン

アンチパターン修正方法
最大ページサイズがないlimit に上限を設定します(例:最大 100)
巨大なテーブルに対する COUNT(*)カーソルページネーションを使用し、合計件数を省略します
数百万行に対するオフセットカーソルまたはキーセットページネーションを使用します
すべてのフィールドを返す必要なフィールドのみを選択し、fields パラメーターをサポートします
デフォルトのソートがない結果を安定させるため、常にデフォルトのソートを定義します

本番環境向けチェックリスト

  • 最大ページサイズが適用されている(例:100)
  • デフォルトのソート順が定義されている
  • 大規模または増加し続けるデータセットにはカーソルページネーションを使用している
  • page/limit/sort パラメーターの入力検証を行っている
  • 一貫したレスポンス形式(data、pagination)を使用している

コメント