pagination について
API のページネーションを実装する開発者向けのリファレンスです。カーソル、オフセット、キーセット方式を比較し、フィルタリングやソート、運用上の注意点も扱います。
主な機能
- 01カーソル・オフセット・キーセット方式
- 02フィルタリングとソートの例
- 03ページサイズと入力検証
- 04本番運用チェックリスト
活用シーン
- 01大規模データの方式選定
- 02管理用エンドポイントへのページング追加
- 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)を使用している
コメント
Markdown 対応