mbvienasbaitas/strapi-laravel-client
Laravel client for the Strapi headless CMS REST API. Query content with a fluent builder supporting filters, pagination, sorting and relation population, plus response caching through Laravel's HTTP client and cache.
Install the package and publish its config:
composer require mbvienasbaitas/strapi-laravel-client
php artisan vendor:publish --tag=strapi-config # if the package ships a config
The config file (typically config/strapi.php) is the first place to look. You need two values almost always:
// .env
STRAPI_URL=https://cms.example.com
STRAPI_TOKEN=your-api-token-here
STRAPI_URL — the base URL of your Strapi instance, without a trailing slash and without /api (the client appends that).STRAPI_TOKEN — a Strapi API token generated under Settings → API Tokens. For read-only content delivery, create a read-only token scoped to the content types you actually expose. Don't reuse an admin JWT.Most usage goes through the facade:
use MbvienasBaitas\Strapi\Client\Facades\Strapi;
$posts = Strapi::collection('posts')->get();
(Exact method names — collection() vs from(), get() vs all() — should be confirmed against the package README; the surface is small, so a five-minute read of src/ is faster than guessing.)
First use case: rendering a blog index. Fetch a paginated list of posts, populate the cover media field and author relation, then hand the result to a Blade view or Inertia page. That single flow exercises config, auth, filtering, populate and pagination — if it works, the rest of the package almost certainly will too.
Before writing feature code, do one Http::fake()-free smoke test via php artisan tinker:
Strapi::collection('posts')->get();
If that returns data, your token scopes and base URL are correct. If it 403s, the problem is Strapi-side permissions, not Laravel.
Wrap the client in a repository/service. Even for a small project, don't call Strapi::collection() directly from controllers. You want one class per content type:
final class PostRepository
{
public function published(int $page = 1): array
{
return Cache::remember("strapi.posts.{$page}", 600, fn () =>
Strapi::collection('posts')
->populate(['cover', 'author'])
->sortBy('publishedAt', 'desc')
->paginate($page, 12)
->get()
);
}
}
This gives you a single place to add caching, retries, fallbacks and response normalization — all of which you will want eventually.
Cache aggressively, invalidate via webhook. Strapi is a network hop; treating it like a database on every request will dominate your TTFB. Cache the shaped response (not the raw Strapi envelope), then add a Strapi lifecycle webhook that hits a Laravel endpoint which flushes the relevant cache keys:
Route::post('/hooks/strapi', function (Request $request) {
abort_unless($request->header('X-Webhook-Secret') === config('services.strapi.hook_secret'), 403);
Cache::tags(['strapi'])->flush();
return response()->noContent();
});
Note Cache::tags() only works with taggable stores (Redis, Memcached, array) — not the file driver.
Normalize media URLs in one place. Strapi returns relative URLs on media objects (/uploads/foo.jpg). Prefix with config('strapi.url') in a resource/transformer, not in Blade templates scattered across the app. Also handle the formats sub-object (thumbnail, small, medium, large) once, centrally.
Use Laravel's HTTP client features via the package, or wrap it. If the client exposes the underlying pending request, you get ->timeout(), ->retry() and middleware for free. If it doesn't, a decorator around the repository is the cleanest place to add resilience:
try {
return $this->repo->published();
} catch (ConnectionException $e) {
report($e);
return Cache::get('strapi.posts.1', []); // stale fallback
}
Test with Http::fake(). The package is a thin wrapper over Laravel's HTTP client, so faking Strapi responses is straightforward and gives you fast, hermetic tests:
Http::fake([
'*/api/posts*' => Http::response(['data' => [...], 'meta' => [...]]),
]);
Keep a couple of real recorded Strapi payloads in tests/fixtures/ — the response envelope is the part most likely to bite you.
Strapi v4 vs v5 response shape. Strapi 4 wraps everything in { data: { id, attributes: {...} } }. Strapi 5 flattens entities so attributes live at the top level. This package targets the v4 shape; if you upgrade Strapi, expect to rewrite your transformers. Pin your Strapi major version alongside the client.
populate is not recursive by default. populate=* gets one level. Deeper relations require explicit syntax (populate[author][populate][avatar]=*), and the package's builder may only expose the simple form. If a nested relation comes back as an empty object, that's almost always a populate depth issue, not a permissions issue.
403 vs 404 confusion. An unpublished or permission-denied content type returns 403 in Strapi, not 404. Check the token's per-content-type permissions in the admin before debugging Laravel. Also remember that find($id) on a draft entry returns 404 unless you have preview/publication permissions.
Default page size is 25, max is 100. If you request =500 you'll silently get 100 and the meta.pagination block will be the only clue. Read meta.pagination.pageCount rather than assuming your request succeeded.
No meta on single-entry endpoints. find() returns bare data, while get() returns data + meta + sometimes error. Don't write one generic unwrapper that assumes both shapes; handle them separately in your repository.
Rate limits and timeouts. Strapi behind a reverse proxy can be slow on cold caches, and the default HTTP timeout will eventually bite you. Set an explicit timeout (5–10s) and consider ->retry(2, 200) for transient 5xx. Do not retry 403/404.
Trailing slashes and base paths. STRAPI_URL=https://cms.example.com/ produces //api/... and some proxies reject it. Likewise, if Strapi is mounted under a subpath (/cms), verify the client respects it — most thin wrappers just concatenate strings.
Trailing config cache. After changing .env values, run php artisan config:clear. This is the single most common "the package is broken" false alarm.
Low-maintenance package — insulate yourself. With ~10 stars and a small maintainer surface, treat this as a thin convenience layer rather than load-bearing infrastructure. The repository/decorator pattern above means swapping it for Http::withToken(...)->get(...) is a one-file change. That's cheap insurance.
Extension points to know. If the package exposes a Strapi singleton in the container, you can swap the binding in a service provider for tests or for a decorated client. Look for a StrapiClient interface or a facades directory in src/ — that's where overrides go. If none exists, keep your repository interface in your own codebase and bind that, not the package class.
How can I help you explore Laravel packages today?