Product API with Pagination, Filtering, and Sorting

mediumTypeScript

Lesson

Building Query-Driven APIs with Pagination, Filtering, and Sorting

Modern APIs need to handle large datasets efficiently while giving clients flexible ways to request exactly the data they need. This is accomplished through query parameters that control three key aspects: pagination (limiting result size), filtering (reducing the dataset), and sorting (organizing results).

Pagination prevents overwhelming both server and client by breaking large datasets into manageable chunks. Instead of returning 10,000 products at once, you return 20 products per "page" and let the client navigate through them. The key insight is that pagination must happen after filtering and sorting to ensure accurate page counts and consistent results.

Filtering allows clients to narrow down results based on specific criteria. Unlike database queries, API filtering often works with multiple independent conditions (category AND price range) that can be combined. Each filter reduces the working dataset before the next operation.

Sorting gives clients control over result order, whether alphabetical, chronological, or by numeric value. The challenge is handling different data types (strings vs numbers) and sort directions while maintaining type safety.

The critical architectural principle is the order of operations: always filter first (to reduce the dataset), then sort (to organize what remains), then paginate (to slice the final results). This ensures your pagination metadata reflects the actual filtered results, not the original dataset size.

Query parameters arrive as strings, so robust parsing with sensible defaults is essential. Invalid values shouldn't crash your API—they should fall back to safe defaults that still provide a useful response.

Example
1// Example: Processing user data with query operations 2interface User { 3 id: number; 4 name: string; 5 role: string; 6 joinDate: Date; 7} 8 9function processUsers(users: User[], filters: any) { 10 // 1. Filter first - reduce the dataset 11 let filtered = users.filter(user => { 12 if (filters.role && user.role !== filters.role) return false; 13 if (filters.since && user.joinDate < new Date(filters.since)) return false; 14 return true; 15 }); 16 17 // 2. Sort second - organize what remains 18 if (filters.sortBy === 'name') { 19 filtered.sort((a, b) => a.name.localeCompare(b.name)); 20 } 21 22 // 3. Paginate last - slice final results 23 const page = parseInt(filters.page) || 1; 24 const limit = parseInt(filters.limit) || 10; 25 const start = (page - 1) * limit; 26 27 return { 28 data: filtered.slice(start, start + limit), 29 total: filtered.length // Total after filtering, before pagination 30 }; 31}
L8Filter first to establish the working dataset size
L15Sort the filtered results, not the original dataset
L21Paginate last, using filtered.length for accurate totals

Key Takeaways

  • •Always apply operations in order: filter → sort → paginate
  • •Query parameters are strings that need parsing with fallback defaults
  • •Pagination metadata should reflect filtered totals, not original dataset size
Loading...