Introduction
Users expect to sort data—newest first, alphabetically, by popularity. A well-designed API lets clients control ordering while protecting against performance issues and injection attacks.
Key Concepts
Ordering: Arranging results based on one or more fields in ascending or descending order.
Multi-field Sorting: Sorting by multiple criteria (e.g., category first, then date).
Sort Direction: Ascending (A-Z, oldest-newest) or descending (Z-A, newest-oldest), often indicated by a - prefix.
Real World Context
Every list endpoint needs sorting:
- E-commerce: Sort by price, rating, newest
- Social: Sort by recent, popular, trending
- Admin: Sort by any column for data management
Deep Dive
Basic Ordering
This view reads the ordering query parameter and validates it against a whitelist of allowed values. The - prefix indicates descending order:
pythondef api_articles(request): # GET /api/articles/?ordering=-created_at ordering = request.GET.get('ordering', '-created_at') # Whitelist allowed fields allowed = ['created_at', '-created_at', 'title', '-title', 'views', '-views'] if ordering not in allowed: ordering = '-created_at' # Default articles = Article.objects.all().order_by(ordering) return JsonResponse({'data': list(articles.values('id', 'title')[:20])})
The whitelist includes both ascending ('created_at') and descending ('-created_at') variants, and falls back to -created_at if the client sends an invalid value.
Multi-field Ordering
Clients can sort by multiple fields separated by commas. Each field is validated independently against the allowed set:
pythondef api_articles(request): # GET /api/articles/?ordering=category,-created_at ordering_param = request.GET.get('ordering', '-created_at') allowed_fields = {'created_at', 'title', 'views', 'category'} order_fields = [] for field in ordering_param.split(','): field_name = field.lstrip('-') if field_name in allowed_fields: order_fields.append(field) if not order_fields: order_fields = ['-created_at'] articles = Article.objects.all().order_by(*order_fields) return JsonResponse({'data': list(articles.values()[:20])})
The order_by(*order_fields) unpacking passes each validated field as a separate argument, and Django applies them in order (primary sort, then secondary, etc.).
Reusable Ordering Mixin
Extract the ordering logic into a mixin so any view can support sorting by declaring ordering_fields and default_ordering:
pythonclass OrderingMixin: ordering_fields = [] default_ordering = '-created_at' def get_ordering(self, request): param = request.GET.get('ordering', self.default_ordering) fields = [] for field in param.split(','): field_name = field.lstrip('-') if field_name in self.ordering_fields: fields.append(field) return fields or [self.default_ordering] def apply_ordering(self, request, queryset): return queryset.order_by(*self.get_ordering(request)) class ArticleAPIView(OrderingMixin, View): ordering_fields = ['created_at', 'title', 'views'] default_ordering = '-created_at' def get(self, request): qs = self.apply_ordering(request, Article.objects.all()) return JsonResponse({'data': list(qs.values()[:20])})
The mixin strips the - prefix before validating field names, so clients can sort any whitelisted field in either direction without listing both variants.
Common Pitfalls
-
Allowing arbitrary fields: Never use
order_by(request.GET.get('sort'))directly. Always whitelist allowed fields. -
Forgetting indexes: Sorting on non-indexed fields causes slow queries on large tables.
-
Case-sensitive sorting:
order_by('title')is case-sensitive in PostgreSQL. UseLower()for case-insensitive.
Best Practices
- Whitelist allowed fields: Only allow sorting on indexed, safe fields.
- Provide sensible defaults: Most recent first is usually expected.
- Document available options: Let clients know which fields support sorting.
- Add database indexes: Index columns that are frequently sorted.
Summary
Ordering lets clients control result order. Always whitelist allowed sort fields, support both ascending and descending with - prefix, and ensure sorted columns are indexed for performance.
Code Examples
def api_articles(request):
ordering = request.GET.get('ordering', '-created_at')
allowed = ['created_at', '-created_at', 'title', '-title', 'views', '-views']
if ordering not in allowed:
ordering = '-created_at'
articles = Article.objects.all().order_by(ordering)
return JsonResponse({'data': list(articles.values('id', 'title')[:20])})