Skip to content

Pagination ​

Guide to paginating large result sets in CodeNotify API.

Overview ​

Endpoints that return lists support pagination to limit response size and improve performance.

Pagination Parameters ​

Query Parameters ​

ParameterTypeDefaultDescription
limitnumber20Number of items per page (1-100)
offsetnumber0Number of items to skip

Example Request ​

bash
GET /contests?limit=20&offset=40

This returns items 41-60 (skipping first 40).

Response Format ​

Paginated responses include metadata:

json
{
  "data": [...],
  "total": 150,
  "limit": 20,
  "offset": 40,
  "hasMore": true
}

Response Fields ​

FieldTypeDescription
dataarrayArray of items
totalnumberTotal number of items
limitnumberItems per page
offsetnumberItems skipped
hasMorebooleanWhether more items exist

Paginated Endpoints ​

Contests ​

bash
# Get first page
GET /contests?limit=20&offset=0

# Get second page
GET /contests?limit=20&offset=20

# Get third page
GET /contests?limit=20&offset=40

Users (Admin) ​

bash
# Get first 10 users
GET /users?limit=10&offset=0

# Get next 10 users
GET /users?limit=10&offset=10

Implementation Examples ​

JavaScript (Fetch All Pages) ​

javascript
async function fetchAllContests() {
  const allContests = [];
  let offset = 0;
  const limit = 50;
  let hasMore = true;
  
  while (hasMore) {
    const response = await fetch(
      `http://localhost:3000/contests?limit=${limit}&offset=${offset}`
    );
    const data = await response.json();
    
    allContests.push(...data.data);
    offset += limit;
    hasMore = data.hasMore;
  }
  
  return allContests;
}

React Pagination Component ​

typescript
import { useState, useEffect } from 'react';

interface PaginationProps {
  endpoint: string;
  limit?: number;
}

const PaginatedList = ({ endpoint, limit = 20 }: PaginationProps) => {
  const [data, setData] = useState([]);
  const [total, setTotal] = useState(0);
  const [offset, setOffset] = useState(0);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    fetchData();
  }, [offset]);
  
  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch(`${endpoint}?limit=${limit}&offset=${offset}`);
      const result = await response.json();
      
      setData(result.data);
      setTotal(result.total);
    } catch (error) {
      console.error('Failed to fetch data:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const totalPages = Math.ceil(total / limit);
  const currentPage = Math.floor(offset / limit) + 1;
  
  const goToPage = (page: number) => {
    setOffset((page - 1) * limit);
  };
  
  return (
    <div>
      {loading ? (
        <div>Loading...</div>
      ) : (
        <>
          <ul>
            {data.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
          
          <div className="pagination">
            <button 
              onClick={() => goToPage(currentPage - 1)}
              disabled={currentPage === 1}
            >
              Previous
            </button>
            
            <span>Page {currentPage} of {totalPages}</span>
            
            <button 
              onClick={() => goToPage(currentPage + 1)}
              disabled={currentPage === totalPages}
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  );
};

Infinite Scroll ​

typescript
import { useState, useEffect, useRef } from 'react';

const InfiniteScrollList = ({ endpoint }) => {
  const [items, setItems] = useState([]);
  const [offset, setOffset] = useState(0);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  const observerRef = useRef(null);
  
  useEffect(() => {
    loadMore();
  }, [offset]);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && hasMore && !loading) {
          setOffset(prev => prev + 20);
        }
      },
      { threshold: 1.0 }
    );
    
    if (observerRef.current) {
      observer.observe(observerRef.current);
    }
    
    return () => observer.disconnect();
  }, [hasMore, loading]);
  
  const loadMore = async () => {
    if (loading) return;
    
    setLoading(true);
    try {
      const response = await fetch(`${endpoint}?limit=20&offset=${offset}`);
      const data = await response.json();
      
      setItems(prev => [...prev, ...data.data]);
      setHasMore(data.hasMore);
    } catch (error) {
      console.error('Failed to load more:', error);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      
      {loading && <div>Loading...</div>}
      
      <div ref={observerRef} style={{ height: '20px' }} />
      
      {!hasMore && <div>No more items</div>}
    </div>
  );
};

Python (Requests) ​

python
import requests

def fetch_all_contests():
    base_url = 'http://localhost:3000/contests'
    all_contests = []
    offset = 0
    limit = 50
    has_more = True
    
    while has_more:
        response = requests.get(base_url, params={'limit': limit, 'offset': offset})
        data = response.json()
        
        all_contests.extend(data['data'])
        offset += limit
        has_more = data['hasMore']
    
    return all_contests

# Usage
contests = fetch_all_contests()
print(f'Total contests: {len(contests)}')

Pagination Strategies ​

Offset-Based (Current) ​

Pros:

  • Simple to implement
  • Can jump to any page
  • Predictable page numbers

Cons:

  • Performance degrades with large offsets
  • Can miss items if data changes

Best for:

  • Small to medium datasets
  • When page numbers are needed
  • When data is relatively static

Cursor-Based (Future) ​

Pros:

  • Consistent performance
  • Handles real-time data well
  • No missing items

Cons:

  • Can't jump to specific page
  • More complex implementation

Best for:

  • Large datasets
  • Real-time data
  • Infinite scroll

Performance Tips ​

Optimize Limit ​

javascript
// Too small - many requests
const limit = 5; // ❌

// Too large - slow response
const limit = 1000; // ❌

// Optimal - balance between requests and response time
const limit = 50; // ✅

Cache Pages ​

javascript
const pageCache = new Map();

async function fetchPage(offset, limit) {
  const key = `${offset}-${limit}`;
  
  if (pageCache.has(key)) {
    return pageCache.get(key);
  }
  
  const response = await fetch(`/contests?limit=${limit}&offset=${offset}`);
  const data = await response.json();
  
  pageCache.set(key, data);
  return data;
}

Prefetch Next Page ​

javascript
async function fetchWithPrefetch(offset, limit) {
  // Fetch current page
  const currentPage = fetchPage(offset, limit);
  
  // Prefetch next page in background
  fetchPage(offset + limit, limit).catch(() => {});
  
  return currentPage;
}

Limits and Constraints ​

Maximum Limit ​

bash
# Maximum limit is 100
GET /contests?limit=100  # ✅
GET /contests?limit=200  # ❌ Returns 400 Bad Request

Validation ​

json
{
  "statusCode": 400,
  "message": [
    "limit must not be greater than 100",
    "offset must not be less than 0"
  ],
  "error": "Bad Request"
}

Best Practices ​

✅ Do ​

  1. Use reasonable limits (20-50 items)
  2. Cache responses when possible
  3. Show loading states during fetch
  4. Handle empty results gracefully
  5. Prefetch next page for better UX

❌ Don't ​

  1. Don't fetch all data at once
  2. Don't use very large limits (>100)
  3. Don't ignore hasMore flag
  4. Don't make concurrent requests for same page
  5. Don't forget error handling

Built with ❤️ for competitive programmers worldwide.