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:

python
def 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:

python
def 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:

python
class 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

  1. Allowing arbitrary fields: Never use order_by(request.GET.get('sort')) directly. Always whitelist allowed fields.

  2. Forgetting indexes: Sorting on non-indexed fields causes slow queries on large tables.

  3. Case-sensitive sorting: order_by('title') is case-sensitive in PostgreSQL. Use Lower() for case-insensitive.

Best Practices

  1. Whitelist allowed fields: Only allow sorting on indexed, safe fields.
  2. Provide sensible defaults: Most recent first is usually expected.
  3. Document available options: Let clients know which fields support sorting.
  4. 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

python
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])})
✓ Completed