Table of Contents
When building sortable lists, the temptation is to handle sorting in JavaScript—intercept clicks, reorder the DOM, maybe use a library like DataTables. But there’s a simpler, more robust approach: let the backend handle it via GET parameters.
The Backend Sorting Pattern
Instead of JavaScript, pass sorting preferences through the URL:
$allowedSorts = ['created_at', 'updated_at', 'name', 'price'];
$sort = request('sort', 'created_at');
$direction = request('direction', 'desc');
if (!in_array($sort, $allowedSorts)) {
$sort = 'created_at';
}
$items = Item::orderBy($sort, $direction)->paginate(20);
Then in your view, generate sort links:
<select onchange="window.location.href=this.value">
<option value="?sort=created_at&direction=desc" {{ request('sort') == 'created_at' ? 'selected' : '' }}>
Newest First
</option>
<option value="?sort=name&direction=asc" {{ request('sort') == 'name' ? 'selected' : '' }}>
Name (A-Z)
</option>
<option value="?sort=price&direction=asc" {{ request('sort') == 'price' ? 'selected' : '' }}>
Price (Low to High)
</option>
</select>
Why Backend Sorting Wins
State in the URL: Users can bookmark a sorted view, share links, and browser back/forward works correctly. With JS sorting, the URL doesn’t change—the state lives only in memory.
Works with pagination: Client-side sorting breaks when you paginate (you’re only sorting the current page). Backend sorting applies across all records.
No Vue template issues: If you’re using Vue, putting <script> tags in templates causes parsing errors. Backend sorting keeps JavaScript out of your Blade/Vue templates entirely.
RESTful and simple: The URL describes the resource state. It’s the way the web was designed to work.
Whitelist Validation is Critical
Notice the $allowedSorts array? This prevents SQL injection via column names:
if (!in_array($sort, $allowedSorts)) {
$sort = 'created_at'; // fallback to default
}
Without this check, a malicious user could inject arbitrary SQL by crafting a URL like ?sort=malicious_column. Always validate sort columns against a whitelist.
Handling Relationships
You can even sort by related model columns using joins:
$allowedSorts = ['created_at', 'name', 'category_name'];
$sort = request('sort', 'created_at');
if ($sort === 'category_name') {
$items = Item::join('categories', 'items.category_id', '=', 'categories.id')
->select('items.*')
->orderBy('categories.name', $direction)
->paginate(20);
} else {
$items = Item::orderBy($sort, $direction)->paginate(20);
}
Preserving Other Query Parameters
If your page has filters or search, append them to sort links using request()->except():
$queryParams = request()->except('sort', 'direction');
$queryParams['sort'] = 'name';
$queryParams['direction'] = 'asc';
<a href="?{{ http_build_query($queryParams) }}">Sort by Name</a>
Or use Laravel’s appends method on paginated results:
{{ $items->appends(request()->except('page'))->links() }}
When JavaScript Sorting Makes Sense
There are still valid use cases for client-side sorting:
- Small datasets (< 100 rows) that fit on one page
- Real-time data that updates frequently via WebSocket
- Interactive tables where instant response is critical (trading dashboards, etc.)
But for most admin panels and user-facing lists, backend sorting with GET parameters is simpler, more robust, and plays nicely with pagination, bookmarking, and server-side rendering.
Let the URL do the work. It’s already there for a reason.
Leave a Reply