API Pagination Strategien | Enjyn Gruppe
Hallo Welt
Hallo Welt
Original Lingva Deutsch
Übersetzung wird vorbereitet...
Dieser Vorgang kann bis zu 60 Sekunden dauern.
Diese Seite wird erstmalig übersetzt und dann für alle Besucher gespeichert.
0%
DE Zurück zu Deutsch
Übersetzung durch Lingva Translate

235 Dokumentationen verfügbar

Wissensdatenbank

API Pagination Strategien

Zuletzt aktualisiert: 20.01.2026 um 11:24 Uhr

API Pagination Strategien

Pagination ist essentiell für performante APIs mit großen Datenmengen. Lernen Sie verschiedene Pagination-Strategien und deren Vor- und Nachteile.

Warum Pagination?

┌─────────────────────────────────────────────────────────────┐
│                   OHNE PAGINATION                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   GET /api/users                                            │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │                                                      │  │
│   │   Response: 100.000 User-Objekte                    │  │
│   │                                                      │  │
│   │   • 50 MB Response                                  │  │
│   │   • 10+ Sekunden Ladezeit                          │  │
│   │   • Datenbank unter Last                           │  │
│   │   • Client-Speicher erschöpft                      │  │
│   │                                                      │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
│                   MIT PAGINATION                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   GET /api/users?page=1&limit=20                           │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │                                                      │  │
│   │   Response: 20 User-Objekte                         │  │
│   │                                                      │  │
│   │   • 10 KB Response                                  │  │
│   │   • < 100ms Ladezeit                               │  │
│   │   • Datenbank effizient                            │  │
│   │   • Gute UX mit Paging UI                          │  │
│   │                                                      │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Pagination-Strategien

Strategie Gut für Probleme
Offset/Limit Einfache Fälle, UI mit Seitenzahlen Performance bei großen Offsets
Cursor/Keyset Große Datenmengen, Real-time Feeds Keine direkten Seitensprünge
Seek/Keyset Sortierte Daten, konsistente Performance Komplexere Implementierung

Offset-Based Pagination

// Request
GET /api/users?page=3&limit=20
// oder
GET /api/users?offset=40&limit=20

// Response
{
    "data": [
        { "id": 41, "name": "User 41", ... },
        { "id": 42, "name": "User 42", ... },
        ...
    ],
    "pagination": {
        "page": 3,
        "limit": 20,
        "total": 1000,
        "totalPages": 50
    },
    "links": {
        "first": "/api/users?page=1&limit=20",
        "prev": "/api/users?page=2&limit=20",
        "next": "/api/users?page=4&limit=20",
        "last": "/api/users?page=50&limit=20"
    }
}
// SQL Implementation
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;  -- Seite 3 mit 20 pro Seite

// PHP Implementation
class UserController {
    public function index(Request $request): JsonResponse {
        $page = $request->input('page', 1);
        $limit = min($request->input('limit', 20), 100);  // Max 100
        $offset = ($page - 1) * $limit;

        $users = DB::table('users')
            ->orderBy('created_at', 'desc')
            ->offset($offset)
            ->limit($limit)
            ->get();

        $total = DB::table('users')->count();

        return response()->json([
            'data' => $users,
            'pagination' => [
                'page' => $page,
                'limit' => $limit,
                'total' => $total,
                'totalPages' => ceil($total / $limit)
            ]
        ]);
    }
}
VORTEILE:
✅ Einfach zu implementieren
✅ Intuitive UI (Seite 1, 2, 3...)
✅ Direkte Sprünge möglich
✅ Total Count möglich

NACHTEILE:
❌ OFFSET scannt alle vorherigen Rows
❌ Performance degradiert bei hohen Offsets
❌ Inkonsistente Ergebnisse bei Änderungen
❌ OFFSET 100000 = DB muss 100000 Rows überspringen

Cursor-Based Pagination

// Request
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20

// cursor ist encoded: {"id": 100}

// Response
{
    "data": [
        { "id": 101, "name": "User 101", ... },
        { "id": 102, "name": "User 102", ... },
        ...
    ],
    "pagination": {
        "nextCursor": "eyJpZCI6MTIwfQ",  // Base64 encoded
        "prevCursor": "eyJpZCI6MTAxfQ",
        "hasMore": true
    }
}
// SQL Implementation
-- Statt OFFSET: WHERE Clause mit letztem Wert
SELECT * FROM users
WHERE id > 100  -- Cursor-Wert
ORDER BY id ASC
LIMIT 20;

// PHP Implementation
class UserController {
    public function index(Request $request): JsonResponse {
        $limit = min($request->input('limit', 20), 100);
        $cursor = $this->decodeCursor($request->input('cursor'));

        $query = DB::table('users')
            ->orderBy('id', 'asc')
            ->limit($limit + 1);  // +1 für hasMore Check

        if ($cursor) {
            $query->where('id', '>', $cursor['id']);
        }

        $users = $query->get();
        $hasMore = $users->count() > $limit;

        if ($hasMore) {
            $users = $users->slice(0, $limit);
        }

        $lastUser = $users->last();

        return response()->json([
            'data' => $users,
            'pagination' => [
                'nextCursor' => $hasMore
                    ? $this->encodeCursor(['id' => $lastUser->id])
                    : null,
                'hasMore' => $hasMore
            ]
        ]);
    }

    private function encodeCursor(array $data): string {
        return base64_encode(json_encode($data));
    }

    private function decodeCursor(?string $cursor): ?array {
        if (!$cursor) return null;
        return json_decode(base64_decode($cursor), true);
    }
}
VORTEILE:
✅ Konstante Performance (unabhängig von Position)
✅ Konsistente Ergebnisse bei Änderungen
✅ Kein OFFSET Overhead
✅ Gut für Infinite Scroll / Load More

NACHTEILE:
❌ Keine direkten Seitensprünge
❌ Kein Total Count ohne zusätzliche Query
❌ Komplexer bei mehreren Sort-Feldern
❌ Opaque Cursor für Client

Keyset Pagination (Multi-Column)

// Bei Sortierung nach mehreren Feldern

// Request
GET /api/posts?cursor=eyJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNSIsImlkIjo1MDB9

// SQL - Mehrere Spalten für eindeutige Sortierung
SELECT * FROM posts
WHERE (created_at, id) < ('2024-01-15', 500)
ORDER BY created_at DESC, id DESC
LIMIT 20;

// Oder mit OR für DB-Kompatibilität
SELECT * FROM posts
WHERE created_at < '2024-01-15'
   OR (created_at = '2024-01-15' AND id < 500)
ORDER BY created_at DESC, id DESC
LIMIT 20;
// JavaScript/TypeScript Implementation
interface Cursor {
    createdAt: string;
    id: number;
}

async function getPosts(cursor?: Cursor, limit = 20) {
    let query = db('posts')
        .orderBy('created_at', 'desc')
        .orderBy('id', 'desc')
        .limit(limit + 1);

    if (cursor) {
        query = query.where(function() {
            this.where('created_at', '<', cursor.createdAt)
                .orWhere(function() {
                    this.where('created_at', '=', cursor.createdAt)
                        .andWhere('id', '<', cursor.id);
                });
        });
    }

    const posts = await query;
    const hasMore = posts.length > limit;

    if (hasMore) posts.pop();

    const lastPost = posts[posts.length - 1];

    return {
        data: posts,
        nextCursor: hasMore ? {
            createdAt: lastPost.created_at,
            id: lastPost.id
        } : null,
        hasMore
    };
}

Performance Vergleich

OFFSET bei 1 Million Rows:

Page 1 (OFFSET 0):     ~5ms   ✅
Page 100 (OFFSET 1980): ~15ms  ✅
Page 1000 (OFFSET 19980): ~120ms ⚠️
Page 10000 (OFFSET 199980): ~800ms ❌
Page 50000 (OFFSET 999980): ~4000ms ❌❌

CURSOR bei 1 Million Rows:

Page 1:      ~5ms   ✅
Page 100:    ~5ms   ✅
Page 1000:   ~5ms   ✅
Page 10000:  ~5ms   ✅
Page 50000:  ~5ms   ✅

→ Cursor-Pagination hat KONSTANTE Performance!

API Response Format

// JSON:API Style
{
    "data": [...],
    "links": {
        "self": "/api/users?page[cursor]=abc",
        "first": "/api/users",
        "next": "/api/users?page[cursor]=xyz",
        "prev": "/api/users?page[cursor]=def"
    },
    "meta": {
        "hasMore": true
    }
}

// HAL Style
{
    "_embedded": {
        "users": [...]
    },
    "_links": {
        "self": { "href": "/api/users?cursor=abc" },
        "next": { "href": "/api/users?cursor=xyz" }
    }
}

// Simple Style
{
    "data": [...],
    "pagination": {
        "cursor": "abc123",
        "nextCursor": "xyz789",
        "hasMore": true,
        "limit": 20
    }
}

Wann welche Strategie?

OFFSET-BASED WÄHLEN WENN:
• Kleine Datenmengen (< 100.000)
• UI mit Seitenzahlen benötigt
• Direkte Seitensprünge nötig
• Total Count wichtig
• Einfachheit Priorität

CURSOR-BASED WÄHLEN WENN:
• Große Datenmengen (> 100.000)
• Infinite Scroll UI
• Real-time Feeds (Twitter, News)
• Performance kritisch
• Daten ändern sich häufig

KEYSET WÄHLEN WENN:
• Sortierung nach mehreren Feldern
• Maximale Performance benötigt
• Konsistente Ergebnisse bei Updates wichtig

Hybrid Approach

// Biete beide Optionen an

// Offset für kleine Requests
GET /api/users?page=1&limit=20

// Cursor für große Requests / Iteration
GET /api/users?cursor=abc&limit=20

// Implementation
class UserController {
    public function index(Request $request): JsonResponse {
        $limit = min($request->input('limit', 20), 100);

        // Wenn Cursor vorhanden: Cursor-Pagination
        if ($request->has('cursor')) {
            return $this->cursorPagination($request, $limit);
        }

        // Sonst: Offset-Pagination
        return $this->offsetPagination($request, $limit);
    }
}
💡 Best Practices: 1. Limit immer mit Maximum begrenzen (z.B. max 100)
2. Cursor-Pagination für große Datenmengen
3. Navigation Links in Response mitgeben
4. Cursor opaque halten (Base64 encoded)
5. Index auf Sort-Spalten für Performance

Weitere Informationen

Enjix Beta

Enjyn AI Agent

Hallo 👋 Ich bin Enjix — wie kann ich dir helfen?
120