This package is in pre-release. The API may change before v1.0.

Query Builder

The QueryBuilder is the main entry point for building API endpoints. It ties together filtering, sorting, includes, and pagination into a single fluent API.

Basic Usage

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductController
{
    public function index(Request $request)
    {
        return QueryBuilder::for(Product::class, $request)
            ->fromResource(ProductResource::class)
            ->paginate();
    }
}

This single call handles:

  • Parsing ?filter[name]=widget and applying allowed filters
  • Parsing ?sort=-created_at,name and applying allowed sorts
  • Parsing ?include=category,tags and eager loading relationships
  • Parsing ?page[number]=2&page[size]=20 and paginating

From a Resource

The fromResource() method pulls filter, sort, and include configuration from your resource class:

use BlueBeetle\ApiToolkit\Parsers\Filters\ExactFilter;
use BlueBeetle\ApiToolkit\Parsers\Filters\PartialFilter;
use BlueBeetle\ApiToolkit\Parsers\Filters\ScopeFilter;
use BlueBeetle\ApiToolkit\Resources\Resource;

class ProductResource extends Resource
{
    public function allowedFilters(): array
    {
        return [
            'name'     => new PartialFilter(),
            'status'   => new ExactFilter(),
            'category' => new ScopeFilter(),
        ];
    }

    public function allowedSorts(): array
    {
        return ['name', 'price', 'created_at'];
    }

    public function defaultSort(): ?string
    {
        return '-created_at';
    }

    public function allowedIncludes(): array
    {
        return ['category', 'tags'];
    }
}

Then in your controller, fromResource() picks up all of these automatically.

Overriding Resource Configuration

Method calls on the QueryBuilder take priority over resource definitions. If you call allowedFilters(), allowedSorts(), defaultSort(), or allowedIncludes() on the builder, those values replace whatever the resource defines. This lets you reuse the same resource class across endpoints while customizing behavior per route.

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\Parsers\Filters\ExactFilter;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductController
{
    public function index(Request $request)
    {
        return QueryBuilder::for(Product::class, $request)
            ->fromResource(ProductResource::class)
            ->allowedFilters(['name' => new ExactFilter()]) // overrides resource filters
            ->allowedSorts(['name'])                         // overrides resource sorts
            ->defaultSort('name')                            // overrides resource default
            ->paginate();
    }
}

From an Existing Builder

Pass an Eloquent builder instead of a model class to start with a scoped query:

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductController
{
    public function active(Request $request)
    {
        $activeProducts = Product::where('is_active', true);

        return QueryBuilder::for($activeProducts, $request)
            ->fromResource(ProductResource::class)
            ->paginate();
    }
}

Pagination Methods

// Offset pagination: ?page[number]=2&page[size]=20
->paginate()

// Cursor pagination: ?page[cursor]=eyJpZCI6MTB9&page[size]=20
->cursor()

// No pagination, returns all results
->get()

Apply Without Fetching

Use apply() to apply filters, sorts, and includes without executing the query. This returns the QueryBuilder instance, so you can call getQuery() to access the underlying Eloquent builder. This is useful when you need the modified query for something other than a standard response.

Counting Results

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductController
{
    public function count(Request $request)
    {
        $builder = QueryBuilder::for(Product::class, $request)
            ->fromResource(ProductResource::class)
            ->apply();

        $query = $builder->getQuery();

        return response()->json(['count' => $query->count()]);
    }
}

Exporting to CSV

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductExportController
{
    public function __invoke(Request $request)
    {
        $query = QueryBuilder::for(Product::class, $request)
            ->fromResource(ProductResource::class)
            ->apply()
            ->getQuery();

        return response()->streamDownload(function () use ($query) {
            $query->each(function ($product) {
                echo $product->name . ',' . $product->price . "\n";
            });
        }, 'products.csv');
    }
}

Custom Response Format

use App\Http\Resources\ProductResource;
use App\Models\Product;
use BlueBeetle\ApiToolkit\QueryBuilder;
use Illuminate\Http\Request;

class ProductStatsController
{
    public function __invoke(Request $request)
    {
        $query = QueryBuilder::for(Product::class, $request)
            ->fromResource(ProductResource::class)
            ->apply()
            ->getQuery();

        return response()->json([
            'total'     => $query->count(),
            'avg_price' => $query->avg('price'),
            'max_price' => $query->max('price'),
        ]);
    }
}

The getQuery() Method

getQuery() returns the underlying Illuminate\Database\Eloquent\Builder instance. You can call it at any time, but it is most useful after apply() so the builder already has all filters, sorts, and includes applied. The returned builder is a standard Eloquent builder, so you can chain any Eloquent method on it.