Home / Blog / Sorting, Filtering, and Paginating Data from a Laravel Backend in a Vue.js SPA
Sorting, Filtering, and Paginating Data from a Laravel Backend in a Vue.js SPA

Sorting, Filtering, and Paginating Data from a Laravel Backend in a Vue.js SPA

Daniel Kelly
Daniel Kelly
March 28th 2023

Searching, filtering, ordering, and paginating are common needs in many applications. This usually applies to items in a table, a grid, or a list. No matter the UI associated with it the process can be pretty straightforward with Laravel and Vue.


If you’re in a hurry, here’s the 20 second spill.

Use Laravel Query Builder and the paginate method on the backend to support query string variables for filtering, ordering, including relationships, paginating and more. On the frontend keep up with your query variables with a reactive ref and remake the request to the backend whenever any of the variables change. Checkout this Stackblitz for an example of what that would look like.

Alright, so let’s get on with the details.

The Laravel Setup

Let’s say we’re dealing with a blogging application and we want to list out all of the posts. For scalability you wouldn’t want to send back every single post within the application, instead you’d want to paginate them. This means you cannot do all the sorting, filter, searching, etc purely in the JavaScript. Instead, you need to make more API requests to handle each action.

Let’s take a look at a controller that might handle such requests.

// app/Http/Controllers/Posts.php
use Spatie\QueryBuilder\QueryBuilder;

class Posts extends Controller {

  function index(Request $request){
    $posts = QueryBuilder::for (Link::class)
      ->allowedFilters(['title', 'views'])
      ->allowedSorts('title', 'created_at', 'views')
      ->paginate($request->get('perPage', 15));
    return response()->json($links);

  // ...

This index method on the Posts controller looks pretty typical but with one primary difference: the use of Laravel Query Builder from

This package allows us to very quickly and easily support filters, sorts, and more from our front end in the form of query string variables.


With Laravel Query Builder in place, a request to the backend that searches for all the posts with the word Laravel in the title would look like this:

GET /posts?filter[title]=Laravel

A filter for posts created at between 2 dates would look like this.

GET /posts?filter[created_at.starts_between]=2022-01-01,2022-12-31

and you could even combine the two.

GET /posts?filter[title]=Laravel&filter[created_at.starts_between]=2022-01-01,2022-12-31


You can also provide a sort query variable to dictate the order of the returned resources. You can control the direction with the inclusion or exclusion of the - prefix.

// sorted by title descending
GET /posts?sort=-title

// sorted by title ascending
GET /posts?sort=title

This is amazing! This provides power to our front-end to make these granular requests for different types of data based on certain filters, sorts, etc, while still leaving control to the backend to secure things with simple allow methods. Plus, besides these, Laravel Query Builder also provides support for including relationships and selecting fields (columns).


Also note, that because we’re using the paginate method this adds support for the page query variable. I’ve also cleverly given control to the frontend over how many posts to send per page.

->paginate($request->get('perPage', 15));

The Vue App

Now that we have the backend prepped and ready for requests, let’s take a look at the Vue side of things.

Defining the Queries as Reactive Data

First, we want to keep track of these different variables for sorting, filtering, and paginating in a reactive way. Easy enough, ref to the rescue!

const queries = ref({
  sort: "",
  "filter[title]": "",
  page: 1,
  perPage: 15

Using the Queries in the Request Query String

Next, we need to include these query variables in the query string of the request that fetches data from our backend.

const data = ref();
const res = await fetch(`/posts?${new URLSearchParams(queries.value).toString()}`)
data.value = await res.json()

Notice that you can quickly and easily convert an object to a query string with the built in browser class URLSearchParams. Here are the MDN docs for this nice utility.

Do note this only works with objects that are one level deep (that’s why I defined the filter above like this: "filter[title]": "" instead of this filter: { title: "" } as someone familiar with PHP might be tempted to do). As a side note, there are JavaScript packages out there that can handle the nested objects. One such package I’ve used before is called qs.

Remaking the Request on Query Changes

So far, we’ve successfully made the first request but the power of Vue and it’s reactivity, is that we can change those filter, sort, and page variables and then automatically re-run the request for the data (with the updated query string in place).

We can do just that with the watch function.

const data = ref();
watch(queries, async ()=>{
  const res = await fetch(`/posts?${new URLSearchParams(queries.value).toString()}`)
  data.value = await res.json()
}, {
  // must pass deep option to watch for changes on object properties
  deep: true,
  // can also pass immediate to handle that first request AND when queries change
  immediate: true

And that’s all there really is to it!

Modify the Query Reactive Ref via the UI

At this point, you can hookup your UI to alter the queries reactive ref however you see fit. For example, for the filter query you’d probably hook it up to a search input.

  placeholder="Search Posts by Title"
  v-model.lazy="queries['filter[title]']" // ⬅️ here's the important part

For the page query, you could hook it up to the laravel-vue-pagination component which comes out of the box support for Laravel’s data structure for pagination responses as well as style for both Tailwind CSS and Bootstrap.

  @pagination-change-page=" = $event"

You could handle the perPage as easily as this.

  Posts Per Page
  <select v-model.number="queries.perPage">
    <option value="10">10</option>
    <option value="25">25</option>
    <option value="50">50</option>
    <option value="100">100</option>

For the sort query, you could hook it up to the table components in many popular Vue.js UI frameworks like Quasar or PrimeVue or roll your own table UI.

Make the Queries Sharable

While everything works with the progress we’ve made thus far, sharing a link to specific pages, filters, and/or sorts would be impossible. Why? Because when the page loads, the value of each of the queries is what we’ve set in the reactive ref.

const queries = ref({
  sort: "",
  "filter[title]": "",
  page: 1,
  perPage: 15

In order to link directly to the data for specific query values, why don’t we support the same query string variables for our application URL, that we’re sending to the backend API.

// When I visit this URL[title]=Laravel&sort=title&perPage=50

// I want this request to be made to the backend[title]=Laravel&sort=title&perPage=50

// notice everything after the ? is the same for both ⬆️

The beauty of Vue.js is that we can knock this out with a single line.

const queries = ref({
  sort: "",
  "filter[title]": "",
  page: 1,
  perPage: 15,
  ...useRoute().query, // ⬅️ here's the magic

With Vue Router, the application url’s query string is already available in object form, so we can spread it into the queries reactive ref definition and any queries present will override the defaults we’ve hardcoded in. This gives us the proper data when the proper variables are provided in the application URL.

Generate the Sharable URL Automatically

To make it easier for the user to know what those proper variables are we can automatically update the application URL whenever the queries reactive ref changes (because the user took some action in the UI). That is one more line added to our watch callback.

watch(queries, async ()=>{
  useRouter().push({ query: queries.value })
}, { /*...*/ })


Getting lists of data from a Laravel backend doesn’t have to be difficult, in fact it can be quite flexible with the Laravel Query Builder package from Likewise, you can hook up a Vue.js front end to flexibly control the API requests with a minimal amount of code.

If you want to see all of this in action together, checkout this Stackblitz playground. In the playground, I cheat just a little bit to get around actually standing up a Laravel backend but you’ll be able to see the Vue implementation clearly.

Also, if you’re interested in learning more about working with Laravel API’s with a Vue.js SPA, then checkout our knowledge packed course: Laravel Backends for Vue.js 3. In it, we build a URL Link Shortener together, and tackle common tasks such as authenticating users, validating form data, quering data flexibly (like in this article), and handle all the other CRUD related tasks as well. Here’s the introductory video to give you a little more insight into what’s in store.

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Latest Vue School Articles

Crafting a Custom Component Library with Vue and Daisy UI

Crafting a Custom Component Library with Vue and Daisy UI

Do you want to build your own component library with Vue.js? We’ll show you how to get started in our course: Crafting a Custom Component Library with Vue and Daisy UI.
Daniel Kelly
Daniel Kelly
What Makes a Good (Vue) Component Library? A Checklist

What Makes a Good (Vue) Component Library? A Checklist

Considering building a Vue Component library? Checkout this checklist of concerns you should make sure to remember!
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 120.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.