Build a Laravel Scout Search Endpoint With the HTTP QUERY Method

Last updated on by

Build a Laravel Scout Search Endpoint With the HTTP QUERY Method image

The HTTP QUERY method is a new addition to the HTTP spec (RFC 10008) designed for exactly one thing: queries. It is a safe, cacheable method that carries its parameters in the request body, giving you the expressiveness of a POST payload without giving up the read-only semantics of GET—and without cramming complex filters into a URL that may hit length limits along the way.

Laravel 13.19 added Http::query() to the HTTP client along with query() and queryJson() testing helpers. A first-class Route::query() helper has been merged for Laravel 14, but you don't have to wait: Laravel's router already accepts custom verbs today via Route::match(). In this tutorial, we'll use it to build a small search endpoint backed by Laravel Scout's database driver.

A single search string like ours doesn't strictly need QUERY—the method starts paying for itself as the request grows into something GET can't carry comfortably:

  • Structured filters with ranges, arrays, and and/or groups (product or listing search)
  • Geospatial queries that send polygon coordinates ("search within this map area")
  • Batch lookups that fetch hundreds of records by ID
  • Searches on sensitive values like emails or phone numbers, which don't belong in URLs, access logs, or browser history
  • Reporting endpoints with date ranges and group-bys that are reads but get modeled as POST /reports/run today

Setting Up

Starting from a fresh Laravel 13 application, install the API routes file and Scout:

php artisan install:api
composer require laravel/scout

Scout's database driver needs no external search service—it uses WHERE LIKE queries against your existing database, which is a good fit for small-to-medium datasets. Enable it in your .env file:

SCOUT_DRIVER=database

Next, create an Article model with a migration and factory:

php artisan make:model Article -mf

The migration defines a title and body:

Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});

Add the Searchable trait to the model and define which columns Scout should search:

namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
 
class Article extends Model
{
use HasFactory, Searchable;
 
protected $fillable = ['title', 'body'];
 
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'body' => $this->body,
];
}
}

Defining a QUERY Route

Laravel's router doesn't support Route::query() in Laravel 13 (it will be available starting in Laravel 14). However, Laravel's router registers routes for any verb you pass to Route::match(), including QUERY.

Add the following to routes/api.php:

use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
 
Route::match(['QUERY'], '/articles/search', function (Request $request) {
$validated = $request->validate([
'search' => ['required', 'string'],
'per_page' => ['sometimes', 'integer', 'between:1,50'],
]);
 
return Article::search($validated['search'])
->paginate($validated['per_page'] ?? 15);
});

Note that validation and $request->input() read from the JSON request body just like they would for a POST request—no extra work required. Running php artisan route:list confirms the route is registered with the QUERY verb:

QUERY api/articles/search

A request to this endpoint looks like a POST with GET semantics—the search criteria travel in the body:

QUERY /api/articles/search HTTP/1.1
Content-Type: application/json
Accept: application/json
 
{"search": "scout", "per_page": 10}

Testing the Endpoint

Laravel 13.19's queryJson() testing helper mirrors postJson() and friends, so exercising the endpoint feels familiar:

namespace Tests\Feature;
 
use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class ArticleSearchTest extends TestCase
{
use RefreshDatabase;
 
public function test_it_searches_articles_with_the_query_method(): void
{
Article::factory()->create(['title' => 'Getting Started With Laravel Scout']);
Article::factory()->create(['title' => 'Queues in Depth']);
 
$this->queryJson('/api/articles/search', ['search' => 'scout'])
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.title', 'Getting Started With Laravel Scout');
}
 
public function test_it_validates_the_search_term(): void
{
$this->queryJson('/api/articles/search', [])
->assertUnprocessable()
->assertJsonValidationErrors('search');
}
}

On the consuming side, Laravel's HTTP client can call QUERY endpoints with the Http::query() method added in 13.19:

use Illuminate\Support\Facades\Http;
 
$response = Http::acceptJson()->query('https://example.com/api/articles/search', [
'search' => 'scout',
]);
 
$articles = $response->json('data');

A Gotcha: PHP's Built-in Server

If you develop with php artisan serve, requests using the QUERY method get a 501 Not Implemented response before they ever reach Laravel—the built-in server hard-codes the request methods it accepts. Nginx-based environments like Laravel Herd and Valet pass the method through to your application. Laravel's HTTP testing helpers avoid the issue entirely since they dispatch requests through the framework directly.

Using QUERY on Web Routes

Our endpoint lives in routes/api.php, so CSRF protection never enters the picture. If you register a QUERY route in routes/web.php, however, you'll hit a 419 error: Laravel 13's PreventRequestForgery middleware only treats GET, HEAD, and OPTIONS as read requests, so QUERY gets the same token check as POST. RFC 10008 defines QUERY as a safe method, and Laravel's next major release exempts it accordingly—but until then, you have two options.

The first option backports the upcoming behavior by extending the middleware and adding QUERY to its list of read verbs:

namespace App\Http\Middleware;
 
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
 
class PreventRequestForgery extends Middleware
{
/**
* Determine if the HTTP request uses a 'read' verb.
*
* QUERY is a safe method (RFC 10008), so treat it like GET.
* Laravel 14 ships this behavior; delete this override after upgrading.
*/
protected function isReading($request)
{
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS', 'QUERY']);
}
}

Then swap it into the web middleware group in bootstrap/app.php:

use App\Http\Middleware\PreventRequestForgery;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as BasePreventRequestForgery;
 
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(replace: [
BasePreventRequestForgery::class => PreventRequestForgery::class,
]);
})

Every QUERY route is now CSRF-exempt while POST, PUT, PATCH, and DELETE keep full protection, matching what Laravel 14 will do out of the box. When you upgrade, delete the class and the replace line and nothing else changes.

The second option opts a single route out of the middleware:

use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
 
Route::match(['QUERY'], '/articles/search', $handler)
->withoutMiddleware(PreventRequestForgery::class);

This works fine for a one-off endpoint, but you'll need to remember it on every QUERY route you add, and it removes the middleware from the route entirely rather than classifying the verb as a read. Since the middleware override is a two-file change that disappears cleanly on upgrade, we'd reach for it first.

One note on verifying either approach: the CSRF middleware skips verification when the application environment is testing, so a feature test won't hit the 419 unless you reassign the environment first (for example, $this->app['env'] = 'production'; in your test's setUp() method).

Finally, remember what the exemption implies: the framework is trusting your QUERY handlers to be read-only, just as it trusts your GET handlers. Don't mutate state in a QUERY route.

Before You Ship QUERY to Production

Laravel handles QUERY well—the routes even work with php artisan route:cache—but the framework is only one hop in the chain. A few things to check before committing to the method in a production API:

  • Every intermediary must pass the method through. We already saw PHP's built-in server return a 501; CDNs, WAFs, and load balancers can be similarly opinionated about verbs they don't recognize. Test the full path from client to app, not just the app.
  • Browser callers have caveats. fetch() sends QUERY without complaint, but HTML forms only speak GET and POST, and QUERY is not a CORS-safelisted method—every cross-origin request will trigger a preflight.
  • The caching benefit is still on paper. The RFC defines QUERY as cacheable, but browsers and CDNs don't implement QUERY caching yet, so don't adopt the method for that win alone today.
  • Spec tooling is catching up. OpenAPI only gained a first-class query operation in version 3.2, released in September 2025—generators and SDK toolchains still targeting 3.0 or 3.1 can't describe the endpoint, and some HTTP client libraries hard-code the verbs they accept.

None of these are dealbreakers for an internal API where you control both ends—which is also the easiest place to try QUERY first.

What's Next

When the next major version of Laravel ships, the Route::match(['QUERY'], ...) call can become Route::query('/articles/search', ...) and the CSRF override above can be deleted—the rest of the code in this tutorial stays the same.

Between routing, the HTTP client, and the testing helpers, Laravel covers both ends of the QUERY method today. You can learn more about the QUERY method in RFC 10008 and follow the routing support in Pull Request #60655.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
SerpApi

The Web Search API for Your LLM and AI Applications

Visit SerpApi
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Laravel Cloud logo

Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Cloud
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media

The latest

View all →
Enforce Per-Action Waiting Periods in Laravel with Cooldown image

Enforce Per-Action Waiting Periods in Laravel with Cooldown

Read article
First-Party Image Processing in Laravel 13.20 image

First-Party Image Processing in Laravel 13.20

Read article
Laravel Legacy Bridge: Carry Authenticated Sessions from a Legacy App into Laravel image

Laravel Legacy Bridge: Carry Authenticated Sessions from a Legacy App into Laravel

Read article
Laravel Quota: Usage Budgets for Calendar Periods image

Laravel Quota: Usage Budgets for Calendar Periods

Read article
Find the Security Vulnerabilities in Your Laravel App with Sensagraph image

Find the Security Vulnerabilities in Your Laravel App with Sensagraph

Read article
Uvora: A macOS Menu Bar App for Finding Laravel Projects image

Uvora: A macOS Menu Bar App for Finding Laravel Projects

Read article