Build a Query Endpoint with NuxtJS and Laravel Eloquent
A sample chapter from Building APIs & Single-Page Applications with Laravel + Vue.JS + CapacitorQuerying an API and allowing the developer to narrow down their results is one of the most important features of developing an API. However, these requests can get EXTREMELY complicated really quickly. The more complex your resource is the more complex your queries become. Maintaining these queries can be a nightmare.
However, it doesn’t have to be. We will walk through the steps of maintaining a large query of a specific resource using Laravel Eloquent. We will then run through a few examples of how to send requests to the specific resource using NuxtJS. Let’s get started!
0. Let’s Plan our Query
Since we are working with ROAST, let’s use our Cafe
resource. In the API we created, we can access this resource by submitting a GET
request to the /api/v1/cafes
endpoint. When you start out building your application you might just run the following code on your API:
$cafes = Cafe::all();
return response()->json( $cafes );
This simply returns every cafe. You can imagine where this is going, though pretty early on. Doing this isn’t convenient or efficient. You don’t really want to return EVERY resource EVERY time. Especially when you get close to one thousand cafes like ROAST has. This is too much data.
The goal is to allow the developer to filter the data that they want returned and then force pagination so they don’t overload the server by asking for too much.
With our Cafe
resource, we allow for querying by the following filters and features:
- A text search of the Location Name, Address, City, State, or Company Name.
- The ability to get only cafes that belong to a certain company.
- Filter cafes that provide a certain brew method, have a drink option available, or certain amenities.
- Return only cafes that are liked by the user.
- Allow for ordering by a column and direction.
- Return pagination so we don’t return every cafe every time.
If you are familiar with Laravel Eloquent queries, you can probably imagine a huge chunk of code to handle this. I’ll show you how I prefer to structure large queries like this, where it’s easy to maintain, implement and provide a beautiful searchable endpoint for your API.
1. Register the API Endpoint
The first step we need to take is to register the API endpoint. To do this open up your /routes/api.php
file. In this file, you should see all of your existing API endpoints. Now, in our example, we did set up the Cafe
resource to be a sub resource of the Company
. It’s alright to step out and make an endpoint to reference this resource directly so we can query the cafe outside of the scope of a company.
What we need to do is in the route group, add the following code:
<?php
Route::group(['prefix' => 'v1'], function(){
//... Other Routes
Route::get('/cafes', 'API\CafesController@search');
});
Since we’ve added this route to our api.php
file and added the v1
prefix, our route will now resolve to a GET
request to /api/v1/cafes
. This will be different depending on what resource you are implementing. It could be /api/v1/users
or /api/v1/podcasts
.
The other thing to notice is since we have implemented the Cafe
as a sub-resource to the Company
in our examples, the standard restful method name on the CafesController
of index()
has been taken. We will be adding a search()
method on the CafesController
that will handle this request.
2. Add the search()
Method
Now that we have our route registered, let’s open up the controller that handles our resource. In our case it’s Http\Controllers\API\CafesController.php
and add the following method:
public function search( Request $request )
{
// Implementation coming soon!
}
For now, this method simply accepts the incoming request which we will pass along to our service for processing. If you haven’t included the use Illuminate\Http\Request
class, make sure you do that as well so you can send the data you need to your service.
I’ve used services a lot in this book so far, because I like the design pattern of keeping your controllers as small as possible. As far as I’m concerned, the less that you have in your controller, the better. This makes for much cleaner code and allows you to break up your business logic appropriately.
With that being said, we are done in this controller. We need to build our search service before we can finish our implementation!
3. Build Search Service
So we have our endpoint and our controller to handle this endpoint. Now we need to implement the logic. To do this, we will abstract this functionality to a service. If we get super detailed queries or need to offload this to a more advanced system like Elastic search or Algolia, we can handle this in our service instead of our controller. For our case, we are doing straight SQL through Eloquent.
Add Your Service
In our API we have an app/Services
directory. Since we are working with our Cafe
resource, we have a app/Services/Cafes
directory so we can keep that namespace localized to any feature and business logic around the Cafe
resource.
For now, let’s add a file called SearchCafes.php
. This can be named whatever you want it to be for your specific service and resource. In that file, start with the following code:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
private $query;
public function __construct( $parameters )
{
$this->setLocalParameters( $parameters );
$this->query = Cafe::query();
}
private function setLocalParameters( $parameters )
{
}
}
We are starting with a shell of a service that starts to build modular queries. The whole idea of building a query in a modular fashion is so it’s maintainable and re-usable. Right away we will see how to do this.
The first thing to note is __construct()
method. It accepts 1 parameter and that is an array of $parameters
. These will get passed in when we create this service. We will be passing in the entire request from the user to our API. Our service will then abstract out these parameters and build a functional search query.
With that being said, the first method we will be calling is the setLocalParameters()
method. We simply pass off the request parameters to this method. We will shell out this method shortly, but we will essentially be applying defaults and setting what the user passed in.
The next step in the construct method is the trick that makes this all work. We set our private $query
variable to be Cafe::query()
:
$this->query = Cafe::query()
By doing this, we can now build out our query in an easily maintainable fashion. Each method of our service can add pieces to the query as you will see when we add a feature. The ::query()
method just initializes a blank Eloquent query for us to use. We can append to it as we go.
Set Up Defaults
There are two aspects of building a query service this way that make this so important. We touched on the modular query which you will see very soon. However, the other most important feature is the ease of setting defaults. Why is this important? You want this query to operate even if the user sends nothing at all. You also want the user not to exploit how much data gets returned and set limits.
Now how do we do this? In our setLocalParameters()
method! Let’s talk about how many items we want to take and order of our query so we can set up our first defaults.
The basics of our query, we want to set up which column and direction the user should order by and how many items we want to return at a time. We will be using Laravel’s Pagination so we will need to define how many items to take. This leaves our variables needed as $take
, $orderBy
, and $orderDirection
. Let’s get those defined on our service:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
private $take;
private $orderBy;
private $orderDirection;
private $query;
//... Other implementation
}
Now, let’s jump back to our setLocalParameters()
method. Remember, this method takes a $parameters
attribute. This will be all of the request variables a user sends along with their request. This could also be entirely empty. So we need to account for all scenarios and set the defaults so our query functions correctly. Let’s add the following code:
private function setLocalParameters( $parameters )
{
$this->take = isset( $parameters['take'] ) ? $parameters['take'] : 6;
$this->orderBy = isset( $parameters['order_by'] ) ? $parameters['order_by'] : 'created_at';
$this->orderDirection = isset( $parameters['order_direction'] ) ? $parameters['order_direction'] : 'DESC';
}
The code may seem plain, but is extremely important, and sets up to scale to our other query parameters as well. Right now, we check the API request to see if the user wishes to override the variable. If it’s set on their request, then we use what they sent. Let’s look at our $take
variable. Say the user want’s more than 6 cafes (or your resource) returned. If they send ?take=12
then we overwrite the default of 6
with what they passed in. On the contrary, if the user doesn’t pass in a take
, then we return 6
cafes by default.
This is the same flow we take for any of our filters. We have an order_by
and order_direction
in this example that operates the same way. We just initialize our private variables with what’s passed in. This way we can use them later on to modify our query. When writing your API documentation, the order_by
should match the column name you wish to order your query by and the order_direction
should be DESC
or ASC
.
We will be going through more examples, but for now, let’s create the method that kicks off this query!
Building The Search Method
This is the main method of our entire service! Also, the only public facing method besides the constructor. The search()
method builds the query with the defaults, runs the query, and returns the results. Let’s start by adding this method to our SearchCafes
service:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
// ... Other methods and set up
public function search()
{
$cafes = $this->query->with('company')->paginate( $this->take );
return $cafes;
}
}
By the time we run this method, we will have instantiated our class and passed the API request variables to the constructor. We will then have all of our private variables set up to what the request entails. Right now, let’s look at:
$cafes = $this->query->with('company')->paginate( $this->take );
This is the guts of the entire service! With our set up, this runs the entire query that was built! We also use the ->paginate()
method provided by Laravel to return a paginated object. We pass the amount of resources we want to take with what was defined. Remember, this can either be set by the user or defaulted. Since we are using Laravel’s paginate, it will return a next
URL and prev
URL which will contain the ?page=X
variable. This will help determine the offset.
Now what about the order we defined? Let’s get that added, by adding a private function like this:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
// ... Other methods and set up
private function applyOrder()
{
$this->query->orderBy( $this->orderBy, $this->orderDirection );
}
}
Then modify the search method with:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
// ... Other methods and set up
public function search()
{
$this->applyOrder();
$cafes = $this->query->with('company')->paginate( $this->take );
return $cafes;
}
}
Now what will happen is our local $query
variable will be ordered by what was either passed in with the API request or the default that we had defined!
Searching Column Names
Now we can implement some more exciting features! Let’s say you want to search by a column name. We can now implement that in an easily maintainable fashion!
For our example, we have a cafe resource and we want to return any results where the location name, address, city, state, or company name match a query parameter. To get started, we need to add the $search
private variable, and define our default like so:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
private $search;
// ... Other private variables
// ... Other methods
private function setLocalParameters( $parameters )
{
$this->take = isset( $parameters['take'] ) ? $parameters['take'] : 6;
$this->orderBy = isset( $parameters['order_by'] ) ? $parameters['order_by'] : 'created_at';
$this->orderDirection = isset( $parameters['order_direction'] ) ? $parameters['order_direction'] : 'DESC';
$this->search = isset( $parameters['search'] ) ? $parameters['search'] : '';
}
}
Next, we need to add our method to apply what the developer is searching for. To do that, add the applySearch()
method:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
private $search;
// ... Other private variables
// ... Other methods
public function applySearch()
{
if( $this->search != '' ){
$search = urldecode( $this->search );
$this->query->where(function( $query ) use ( $search ){
$query->where('location_name', 'LIKE', '%'.$search.'%')
->orWhere('address', 'LIKE', '%'.$search.'%')
->orWhere('city', 'LIKE', '%'.$search.'%')
->orWhere('state', 'LIKE', '%'.$search.'%')
->orWhereHas('company', function( $query ) use ( $search ){
$query->where( 'name', 'LIKE', '%'.$search.'%' );
});
});
}
}
}
Let’s break this down a little bit. Remember, our query is entirely modular so we can choose to leave out certain parts if we don’t need them. This is a prime example. Right off the bat we have the if( $this->search != '' )
line. Essentially, our default for search string is an empty string. If the developer didn’t send an override, then we don’t even run this method!
However, if there is a search token, now we can build our query string to search each column that we want to make available for search. If you have a large resource and really want to, you could even pass along which columns you want to search.
The first thing we want to do is scope your query into a sub query. Why would you do that? If you have other methods (which you most likely will) that require a where
statement, you don’t want to apply a ton of orWhere
statements or the logic of your query will get messed up. Say you have a “where company is X” and an “or where state is Y”. Those are two different scopes. You don’t want to find all cafes that belong to a company OR have a state. You want to search for all cafes that have a certain company where the state is X.
If you wrap all of your orWhere
statements in a callback like we do here:
$this->query->where(function( $query ) use ( $search ){
});
we can limit our search results and orWhere
to a single scope that doesn’t affect the other modular parts of our query. Within this callback, we pass our $search
variable which is set above this logic with:
$search = urldecode( $this->search );
Now we have access to our queried parameter and can compile our orWhere
statements in the logical format we need. At this point, we can add any column we wish to search by chaining it to our query!
The only unique statement we have chained is the orWhereHas
at the very end. This is a super powerful method that runs on a related model. In this case we also search the name of the parent Company
model to return.
Finally, we just need to call this method in our search()
method like this to build our query:
public function search()
{
$this->applySearch();
$this->applyOrder();
$cafes = $this->query->with('company')->paginate( $this->take );
return $cafes;
}
That’s it! Now our entire logic for searching is added to our modular query. See where this is going? If not, the next example will show a little more of the power of modular queries. You now can add the ?search=X
at the end of your GET request and your API will return paginated, filtered results!
Has Many to Many Relationship
On our Cafe resource, we have a variety of many to many relationships. For example, our cafe has many brew methods, has many amenities, has many drink options. In the future, there will probably be more! Querying these relationships is extremely important for providing powerful filters to your developers.
All of the many to many relationships can be queried the same way, so for our example, let’s just go through one of them, Brew Methods.
Like we did in the previous section, the first step is to add a private variable in our service that we can set defaults for and store request data:
private $brewMethods;
Next, we need to add the following line to our setLocalParameters()
method:
$this->brewMethods = isset( $parameters['brew_methods'] ) ? $parameters['brew_methods'] : '';
We are going a little quicker through this because it’s the same as above. We will get into the meat and potatoes of this soon. But first, let’s take a quick step back. Think about how you want users to query your API for many to many relationships. If we pass data for brew methods, we’d like to get all cafes that have the brew methods available. We will have to send multiple ids somehow. Since this is a GET request, we will be doing that through the URL. This means we will be separating IDs by commas.
For example, our request will look like: /api/v1/cafes?brew_methods=1,3,6
and we want all cafes that offer those brew methods. Let’s make that happen!
Now that we have our defaults, add the following method to your service:
public function applyBrewMethodsFilter()
{
if( $this->brewMethods != '' ){
$brewMethodIDs = explode( ',', urldecode( $this->brewMethods ) );
$this->query->whereHas('brewMethods', function( $query ) use ( $brewMethodIDs ){
$query->whereIn( 'id', $brewMethodIDs );
});
}
}
It’s a relatively small method, but let’s break it down. First and foremost, we check to see if the $this->brewMethods
is set to anything. If it’s not, we don’t need to run the method!
However, if it is set, then we need to explode the list by the ,
. This will give us an array of IDs that we are looking for.
Next, we can run the ->whereHas()
method on the many to many brewMethods
relationship. This can be any many to many relationship your app has set up. We then use the whereHas
scope, to return any cafes that have the relationship to brew methods.
How do we do that? We use the whereIn
method. Essentially this allows us to pass an array of IDs of the relationship we are looking for and return only the resources that have that relationship. Pretty nifty isn’t it?
Finally, we just have to call this method from within our search()
method like this:
public function search()
{
$this->applySearch();
$this->applyBrewMethodsFilter();
$this->applyOrder();
$cafes = $this->query->with('company')->paginate( $this->take );
return $cafes;
}
We can repeat this modular functionality with any many to many relationship and make our search service that much more advanced! This should give a basic overview of how to construct these services and really make your GET
endpoint flexible and powerful.
Important Notes
When writing your query service, there are a few things to note.
As you see when we add our other filters to the query, the applyOrder()
method is always LAST in our search()
method. The reason for this is so the query gets constructed properly within Eloquent. It should follow the order WHERE, WITH, ORDER to ensure your query gets constructed correctly.
Another reason we abstracted to a service and broke up into separate methods is so we can perform logic on how the query is constructed. Feel free to break up any of these methods into even smaller methods or services! It helps maintain code readability and helps you add more filters as needed.
One place where we did this is with the applyOrder()
method. Say you wanted to get the newest resources (in our case cafes). You’d send a GET request to /api/v1/cafes/?order_by=created_at&order_direction=DESC
. But what if you wanted to order by the most liked cafes (or any other computed value)? You’d like to pass something like /api/v1/cafes/?order_by=likes
. In your applyOrder()
method, you can perform extra logic like this:
<?php
namespace App\Services\Cafes;
use App\Models\Cafe;
use Auth;
class SearchCafes {
// ... Other methods and set up
public function applyOrder()
{
switch( $this->orderBy ){
case 'likes':
$this->query->withCount('likes as liked')
->orderByRaw('liked DESC');
break;
default:
$this->query->orderBy( $this->orderBy, $this->orderDirection );
break;
}
}
}
After adding that piece, you can either pass in a column name or a specific computed value. These methods can get pretty complicated, so like I mentioned, break them up at will to maintain code re-usability.
4. Implement Search Method on Controller
Now that we have our our service created, let’s quickly add it to our controller so we can return the results! In our case, let’s re-open the Http\Controllers\API\CafesController.php
and find our search()
method. Then add the following code:
public function search( Request $request )
{
$searchCafes = new SearchCafes( $request->all() );
$cafes = $searchCafes->search();
return response()->json( $cafes );
}
Those 3 lines of code are all we need to hook everything together. We simply create a new SearchCafes
service and pass along the entire request from the user.
Next, we run the search()
method on that service and return the results. All the business logic is decoupled to the service so our controller is nice and clean! Just make sure you remember to add the following to the top of your controller:
use App\Services\Cafes\SearchCafes;
That’s it! We now have a functioning endpoint that we can run multiple queries on. For a wide variety more queries, make sure you check out the code on GitLab (Note: Gitlab access is available in our “Complete Package“). Let’s take a look at a few examples we can run!
5. Example Requests
Search for Cafes Named “Ruby”
GET /api/v1/cafes?search=Ruby
Find All Cafes With Pour Over or French Press
GET /api/v1/cafes?brew_methods=1,8
Find All Cafes Named “Collectivo” with Pour Over
GET /api/v1/cafes?search=Collectivo&brew_methods=1
6. Send Request from NuxtJS
Finally, we have to allow users to send requests from our SPA to the API. Luckily we abstracted all of our API resources to their own separate plugin and files to manage this easily!
In the front end we have our cafes
API abstraction in /api/cafes.js
. If you are following along, this will be similar. We need to add the search method that maps to our /api/v1/cafes
endpoint like this:
export default $axios => ({
async search( params ){
try {
return await $axios.$get('/api/v1/cafes', {
params: params
});
} catch ( err ){
}
},
// ... other endpoints
})
We now have a search()
method on our $api
plugin. The first parameter we pass to $axios.$get
is the URL of our API endpoint.
The second parameter is the configuration for the GET
request required by axios. In that JSON formatted parameter, we have a params
key. Axios will then combine that to the end of the URL to form a proper request.
Now, whenever we need to run a search of our resource, we can run:
async searchCafes(){
this.cafes = await this.$api.search({
page: 1,
search: 'Ruby',
brew_methods: '1,3,6'
});
}
Pretty nifty! Seems like a lot of work, but as you start creating more and more query options, having a standard in place for how to add them makes the process a lot easier!