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.
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}