Laravel API Resources with Relations: Methods to Avoid N+1 Query

Laravel has API Resources but, loaded with relationships, they may cause performance issues. There are conditional methods to help you avoid it.

These methods allow you to show/hide attributes/relationships based on certain conditions. For example:

  • If a relationship is not loaded, don't return it
  • If a field is null, don't return it
  • If a model is missing an attribute, don't return specific data
  • And so on...

Let's look at these methods.


When Loaded

Of course, there is a dedicated method to only return a relationship if it's loaded (to avoid N+1 issues):

Controller

$apartments = Apartment::query()
->with(['amenities']) // This has to have the relationship
->paginate(10);

Then, when using it with the resource:

Resource

'amenities' => AmenityResource::collection($this->whenLoaded('amenities')),

Under the hood, it calls relationLoaded() on the Model to see if it was eager loaded.

Vendor

protected function whenLoaded($relationship, $value = null, $default = null)
{
if (func_num_args() < 3) {
$default = new MissingValue;
}
if (! $this->resource->relationLoaded($relationship)) {
return value($default);
}
$loadedValue = $this->resource->{$relationship};
 
if (func_num_args() === 1) {
return $loadedValue;
}
if ($loadedValue === null) {
return;
}
if ($value === null) {
$value = value(...);
}
return value($value, $loadedValue);
}

This can prevent you from accidentally having N+1 issues in your API, but that's not all. You can also Hide/Show relationships based on the Model's state.

  • No eager loading? No relationship in the response!
  • Eager loaded? The relationship is in the response!

A great way to add some control over your API responses. Especially if combined with Query parameters.


Check if the Model has an Attribute

Some attributes depend on other attributes. For example, if you have a price attribute, you can calculate the tax attribute based on it:

Resource

'price' => $this->price,
'tax' => $this->whenHas('price', fn() => number_format($this->price * 0.21, 2)),

This will only return the tax attribute if the price attribute exists on the Model. It will not return an attribute that has not been loaded from the database.

Code under the hood:

Vendor

public function whenHas($attribute, $value = null, $default = null)
{
if (func_num_args() < 3) {
$default = new MissingValue;
}
 
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
 
return func_num_args() === 1
? $this->resource->{$attribute}
: value($value, $this->resource->{$attribute});
}

It's a great way to prevent errors in your API if some attributes are missing. For example, if this API query did not return the price field, it would not return the tax field either.


Check if the Attribute is Appended

A lot of API endpoints can contain custom attributes. These attributes usually come as Appends in Models. Using the whenAppended() method, you can conditionally return the attribute:

Model

protected function addressLines(): Attribute
{
return Attribute::make(
get: fn($value) => explode(',', $this->address),
);
}

You can use the whenAppended() method to conditionally return the attribute:

Resource

'address' => $this->address,
'address_lines' => $this->whenAppended('address_lines', $this->address_lines),

This will not return the address_lines attribute if we don't have the address_lines appended to the Model:

Controller

public function index(): ApartmentCollection
{
$apartments = Apartment::query()
->with(['amenities'])
->withExists('amenities')
->paginate(10);
 
$apartments->append('address_lines');
 
return new ApartmentCollection($apartments);
}

Under the hood:

Vendor

protected function whenAppended($attribute, $value = null, $default = null)
{
if ($this->resource->hasAppended($attribute)) {
return func_num_args() >= 2 ? value($value) : $this->resource->$attribute;
}
 
return func_num_args() === 3 ? value($default) : new MissingValue;
}

Once again, we can easily control when to return the attribute based on the Model's state. If the attribute is not appended, it will not return the attribute.


Return Attribute if Counted

Some API endpoints should only return data if it's counted. For example, if you have a relationship that is counted, you can use the whenCounted() method:

Resource

'amenities_count' => $this->amenities_count,
'amenities' => $this->whenCounted('amenities', AmenityResource::collection($this->amenities)),

This will not return the relationship if we don't have the count of the relationship:

Controller

$apartments = Apartment::query()
->with(['amenities'])
->withCount(['amenities'])
->paginate(10);

This is useful when the relationship needs to be returned only if it's counted. If the count is not loaded, it will not return the attribute.

Under the hood:

Vendor

public function whenCounted($relationship, $value = null, $default = null)
{
if (func_num_args() < 3) {
$default = new MissingValue;
}
 
$attribute = (string) Str::of($relationship)->snake()->finish('_count');
 
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
 
if (func_num_args() === 1) {
return $this->resource->{$attribute};
}
 
if ($this->resource->{$attribute} === null) {
return;
}
 
if ($value === null) {
$value = value(...);
}
 
return value($value, $this->resource->{$attribute});
}

This way, you can easily control when to return to the relationship. Especially if a count is involved - you instantly inform the API consumer if the relationship is empty.


Only When Exists and Loaded - New in Laravel v11.20

Some relationships can only be returned if they exist and are loaded. For that, we use the whenExistsLoaded method:

'amenities' => $this->whenExistsLoaded('amenities', AmenityResource::collection($this->amenities)),

This will only return the attribute if both of the following conditions are met:

$apartments = Apartment::query()
->with(['amenities']) // We need to load the relationship
->withExists('amenities')// And make sure it exists
->paginate(10);

Otherwise, it does not load anything and does not return the attribute.

Under the hood, there is a simple check:

public function whenExistsLoaded($relationship, $value = null, $default = null)
{
if (func_num_args() < 3) {
$default = new MissingValue;
}
$attribute = (string) Str::of($relationship)->snake()->finish('_exists'); // Checks if an attribute exists...
 
if (! array_key_exists($attribute, $this->resource->getAttributes())) {
return value($default);
}
if (func_num_args() === 1) {
return $this->resource->{$attribute};
}
if ($this->resource->{$attribute} === null) {
return;
}
return value($value, $this->resource->{$attribute});
}

This checks for the attribute amenities_exists on the model. If it's not there, it returns the default value or nothing.


If you want to learn more about APIs, we have a course How to Build Laravel 11 API From Scratch

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 64 courses (1141 lessons, total 42 h 01 min)
  • 88 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent New Courses