Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Search Api Laravel Package

liberusoftware/search-api

A Laravel package that provides a fluent API for building and running search queries, letting you define searchable models, apply filters and sorting, and return paginated results through a clean, consistent interface.

View on GitHub
Deep Wiki
Context7

Getting Started

This module is the HTTP skin over a search backend — it does not implement search itself. Expect authenticated, rate-limited routes that accept a query and delegate to liberusoftware/search (or whatever engine you bind). That means setup is two jobs: install the transport, then make sure there is something behind it.

Before you composer require, do five minutes of due diligence, because the public signal is thin (0 stars, 0 dependents, no downstream projects to crib from):

composer show liberusoftware/search-api --all   # requires, suggests, versions
# on GitHub: read composer.json requires, CHANGELOG, and src/Http/Controllers

Then:

composer require liberusoftware/search-api
php artisan vendor:publish --provider="Liberu\SearchApi\SearchApiServiceProvider"
php artisan route:list --path=search

route:list is your map. Read it before writing any client code — it tells you the URI prefix, the middleware stack, and the exact parameter names. Do not assume /api/search; the module may mount under a configurable prefix or version segment.

Auth is the first decision. If your consumers are SPAs, check whether the routes ship with auth:sanctum (stateful cookie flow) or auth:api (bearer tokens). If they're server-to-server, mint a token with a narrow ability and confirm the middleware actually checks it:

$token = $user->createToken('search-client', ['search:read'])->plainTextToken;

Rate limiting is the second. Find the limiter the module registers — typically in its service provider via RateLimiter::for('search-api', fn ($request) => Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())) — and confirm the cache store backing it isn't array.

First use case: get one query through end-to-end from a real client, including the failure paths.

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://app.test/api/search?q=invoice&per_page=10" | jq

If that returns 401, 429, or 500, you have found the three things you'll actually spend your first day on. Fix those before building features.

Implementation Patterns

Treat it as a bounded context, not a controller you edit. The highest-value pattern is to leave the vendor routes alone and add a thin façade in your app that owns your public contract:

// routes/api.php
Route::prefix('v1')->middleware(['auth:sanctum', 'throttle:search-api'])->group(function () {
    Route::get('/search', SearchProxyController::class);
});

The proxy forwards to the module (via HTTP client or by resolving the module's service from the container) and reshapes the response. This buys you versioning, response stability, and the ability to swap the module out later without breaking clients.

Normalize the response into a resource. Search APIs drift on pagination shape. Pin it:

return SearchResultResource::collection($result->hits())->additional([
    'meta' => [
        'total'    => $result->total(),
        'page'     => $result->page(),
        'per_page' => $result->perPage(),
        'took_ms'  => $result->took(),
    ],
]);

Scope by tenant, always. For multi-tenant apps the classic bug is a search endpoint that silently searches everything. Either the module supports a filter scope, or you wrap it:

// middleware or controller
$query = $request->string('q');
$filters = ['tenant_id' => $request->user()->tenant_id];

Never let a client-supplied filter[tenant_id] override the server-derived one. Merge server-side after the client payload.

Handle indexing as an event-driven concern. The read API is only as good as the write path. Wire model observers to dispatch index/update/delete jobs onto a queue, and keep them idempotent — reindex jobs get retried. Run a full reindex as a scheduled command, not as a request.

Cache at the edge of the search call, keyed by the normalized query. Search is expensive and often repeated:

$key = 'search:'.hash('xxh128', json_encode([$tenantId, $q, $filters, $page]));
$result = Cache::remember($key, now()->addMinutes(5), fn () => $engine->search(...));

Invalidate by bumping a per-tenant version key on write rather than tagging (tags aren't supported on all stores, and file/database stores silently ignore them).

Testing: fake the limiter, not the auth. Use Sanctum::actingAs($user) and clear limiter state between tests, otherwise your second test in a file gets a 429 and you lose an hour:

protected function setUp(): void
{
    parent::setUp();
    RateLimiter::clear('search-api');
}

Add one contract test that asserts the shape of the response (keys, types, pagination fields). That's the test that survives a vendor upgrade.

Version before you ship. If you expose this to third parties, the module's route prefix is your API version. Confirm it's settable via config so you're not re-mounting routes to introduce /v2.

Gotchas and Tips

  • The release date is ahead of the ecosystem. Last release listed as 2026-10-27 with zero dependents means almost nobody has run this in anger. Pin the exact version ("liberusoftware/search-api": "1.2.3"), read CHANGELOG.md before every bump, and assume no Stack Overflow answers exist. Budget time to read src/.
  • Config caching breaks late-bound values. If the prefix, guard, or limiter name is read from config() inside a route file, php artisan config:cache in production can freeze a value you changed. Verify with php artisan route:list after caching.
  • Rate limiter cache store. The limiter uses the default cache store. If that's array in local/testing, throttling appears to work and then vanishes; if it's file behind multiple app servers, each node gets its own counter. Use Redis for anything multi-node, and confirm Retry-After and X-RateLimit-* headers are actually emitted.
  • Auth middleware duplication. Modules frequently ship with their own middleware registered and the host app adds auth:sanctum in a route group. Double application is harmless for auth but will double-count rate limits if throttle is also stacked. Inspect the middleware on route:list before adding yours.
  • SPA CSRF vs bearer tokens. Sanctum's stateful mode requires EnsureFrontendRequestsAreStateful and correct SANCTUM_STATEFUL_DOMAINS. A 419 on a search call is this, not the module.
  • 404 vs empty result set. Decide the contract explicitly. A no-hits search should be 200 with an empty data array; a malformed query should be 422. If the module returns 404, normalize it in your proxy — clients will not handle it gracefully.
  • Deep pagination is a trap. per_page caps and max offset/from limits should be enforced server-side. If the module doesn't cap them, clamp in your middleware or you'll hand clients a one-request way to take down the engine.
  • Query string injection. If the module passes q straight to a query DSL (Elasticsearch/Meilisearch), validate and length-limit it. Reject arrays where a string is expected — ?q[]=x producing a 500 is a common finding.
  • Error rendering. API routes need JSON errors. Confirm bootstrap/app.php (Laravel 11+) or the exception handler uses shouldRenderJsonWhen for api/* or the module's prefix, otherwise 500s come back as HTML and your HTTP client throws something useless.
  • Extension points to look for: a container binding or config key for the underlying engine/client, a publishable config/search-api.php, a SearchApi facade, response resources you can override, and middleware you can insert. If the controller is final and resolves the engine directly with new, you have no seam — proxy at the HTTP layer instead of fighting it.
  • Debugging checklist, in order: route:list (is the route there?) → middleware stack (is it my auth or theirs?) → RateLimiter::clear() (is it a test artifact?) → tinker the engine binding directly (is it the module or the backend?) → Log::debug the outgoing query. Most failures are in the first two steps.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console