Filament: Load Table Data From 3rd Party API

When working with Filament, your table may need to load data from a 3rd party API system. Filament does not support this out of the box, so you have to use a 3rd party package called Sushi. And even then, to be honest, the functionality is pretty limited. But let's see how to do it for simple cases.

Imagine you need to load this table data from the external API that returns the list of subscriptions:

You would think you can use the ->viewData() method of Filament Tables with JSON data?

public function table(Table $table): Table
{
$apiCall = Http::asJson()
->acceptJson()
->get(config('app.url') . '/api/subscription');
 
return $table
// You might think this works, but it does not.
// It's not meant for this use-case
->viewData($apiCall->json('data'))
->columns([
TextColumn::make('user_name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
]);
}

But you will encounter an immediate issue if you try to use a table, as it requires the query() function to be called:

Now let's see how to make it work, with Sushi package.


What is Sushi and How Does It Work?

"Sushi" is a package that can create a fake model with its own SQLite database from any array data source. It's used like this:

app/Models/SubscriptionApiWrapper.php

use Http;
use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;
 
class SubscriptionApiWrapper extends Model
{
use Sushi;
 
public function getRows(): array
{
$apiCall = Http::asJson()
->acceptJson()
// This uses a local API endpoint for demonstration purposes.
// You can use any API endpoint you want
->get(config('app.url') . '/api/subscription');
 
return $apiCall->json('data');
}
}

Usage of this Model is the same as any other Eloquent Model:

app/Filament/Pages/SubscriptionsList.php

use App\Models\SubscriptionApiWrapper;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
 
class SubscriptionsList extends Page implements HasTable
{
use InteractsWithTable;
 
protected static ?string $navigationIcon = 'heroicon-o-document-text';
 
protected static string $view = 'filament.pages.subscriptions-list';
 
public function table(Table $table): Table
{
return $table
// Here we are taking the Sushi model and passing it as our query
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user_id'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
]);
}
}

The display of the table is handled as usual:

resources/views/filament/pages/subscriptions-list.blade.php

<x-filament-panels::page>
{{ $this->table }}
</x-filament-panels::page>

Finally, loading the page will give us a table with the data from the API:

This works perfectly fine to display a simple table, but when you need to add some modifications to the table, you might run into issues. So let's see a few common ones to look out for.


Issue 1 - No Relationships

As you might have noticed, we use the user_id instead of the user name. But if we look at our API response - we can see that there is a user object that contains the user name:

Let's try telling Filament that this is a relationship:

app/Filament/Pages/SubscriptionsList.php

// ...
public function table(Table $table): Table
{
return $table
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user_id'),
TextColumn::make('user.name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
]);
}

Now, if we attempt to load our table - we'll have no data:

This happens because Filament expects a user relationship to be present on the SubscriptionApiWrapper model. But since we are using a fake model - we don't have any relationships. Of course, we can solve this by doing something nasty:

app/Filament/Pages/SubscriptionsList.php

// ...
public function table(Table $table): Table
{
return $table
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user.name'),
TextColumn::make('user_name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
]);
}

And then in our Wrapper:

app/Models/SubscriptionApiWrapper.php

// ...
public function getRows(): array
{
$apiCall = Http::asJson()
->acceptJson()
->get(config('app.url') . '/api/subscription');
 
return collect($apiCall->json('data'))
->map(function ($item) {
// Sushi does not support Array fields, so you have to move them to new fields
$item['user_name'] = $item['user']['name'];
unset($item['user']);
 
return $item;
})
->toArray();
}

And now, we have our user name in the table:

But this seems like much work for something quite simple. And now imagine if you have multiple relationships available like this - how much work would you have to do to make this work? Not a great solution.

Note: Sushi has relationship support if used in the context of internal application. In this case, it fails to provide relationships in the Laravel way (via the belongsTo method) as it uses a separate local sqlite database to store all the records in our Model. Casting


Issue 2 - No Filters on Table

Another issue that we might run into is the need for filters. Let's say we want to filter our subscriptions by the user name. We can add a filter to our table:

app/Filament/Pages/SubscriptionsList.php

public function table(Table $table): Table
{
return $table
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user_name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
])
->filters([
SelectFilter::make('user_id')
->relationship('user', 'name')
]);
}

Now try to run this - you'll get an error:

That's because we are trying to load a relationship that does not exist on our fake model. We can try to add it:

app/Models/SubscriptionApiWrapper.php

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
// ...
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

And now, we will get a filter on our table:

But if we attempt to filter by it - we'll get an error:

This happens because Sushi, by default, creates a single file database that's running on an SQLite engine and does not have the users table. We can, of course, apply a little hack to make this work:

app/Filament/Pages/SubscriptionsList.php

public function table(Table $table): Table
{
return $table
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user_name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
])
->filters([
SelectFilter::make('user_id')
->relationship('user', 'name')
->options(User::pluck('name', 'id'))
]);
}

Which will filter our table without any calls to the database:

But that might be better if you have a few records. And again, this is a lot of work for something that should be simple.


Issue 3 - Pagination Limitations

Another issue that we might run into is the need for pagination. The API returns you ten subscriptions at a time. How do you go to a second page? Well, you can't. At least not by using this code:

app/Filament/Pages/SubscriptionsList.php

// ...
public function table(Table $table): Table
{
return $table
->query(SubscriptionApiWrapper::query())
->columns([
TextColumn::make('user_name'),
TextColumn::make('name'),
TextColumn::make('price'),
TextColumn::make('start_date'),
TextColumn::make('end_date'),
])
->filters([
SelectFilter::make('user_id')
->options(User::pluck('name', 'id'))
])
->paginated();
}
// ...

This does show us the page selection at the bottom:

And we can go to the next page:

But that's where it ends. We cannot load more than one page of data from the API itself, so we are limited to only what we receive from the API. For example, if the API returned 20 records, we can paginate in those 20, but only some of the ones the API might have. That's a significant limitation.


Conclusion - You Might Want To Rethink The Choice

We can make Filament work with a 3rd party API, but it could work better.

You will have to do a lot of work to make it fully compatible with Filament, and even then, you will run into some limitations.

A better option might be skipping Filament usage with the 3rd party API and building everything custom or even using a sync way (to sync API resources to local models) for better results.


Notice: this article was written in Autumn 2023, so things might have changed if you landed here months later. Let us know in the comments if that's the case, and we will update the article!


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

avatar

Great read! I was considering doing the exact same thing and now I know I should spend more time inspecting the case. Thank you!

👍 3
avatar
Eugene van der Merwe

This is really useful. I spend my life retrieving data from remote APIs. It's good to see how it can be done with something like Sushi. It's a challenging aspect of working with multiple datasets because you can spend your life building synchronization scripts, and when they break it's just a mission.

avatar

While sushi is a great way to solve that - it gives a different set of challenges for you!

There are a lot of cases where the synchronization scripts are better than this approach, but in some small use-cases this is good enough

avatar

I'm curious whether Sushi is the best and the only way to create tables in Filament from collections or arrays. After conducting some research, I have found that the table query in Filament does not support collections. Let's assume that I want to create a table from two or more concatenated collections of my models, not only from the data received from an API. In this case, should I use Sushi, or are there any other possibilities available for achieving this?

avatar

You are correct, the table builder does not accept an array. It will always require a query() to be present, which means that you must have a model available.

So it does not matter if it is an API or just a collection put into a table - you need a package like Sushi to help you there. I have not found any other way to get the data into a table

Like our articles?

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

Recent Premium Tutorials