Laravel Rest Api 3 (the lomkit/laravel-rest-api package) turns your Eloquent models into a full REST API from one resource class per model. Version 3.0 was a near-rewrite, with typed fields, OpenAPI docs through Scramble, metric endpoints and confirmations for dangerous writes, and the 3.x line is moving fast: 3.3.0 landed on 12 November 2026 and 3.4.0 on 13 November. This tutorial shows how to install it, generate your first resource, send search and mutate requests, and use the newest 3.3 and 3.4 features.
What the package does
Instead of writing a controller method for every list screen and form, you declare what the API exposes for each model: its fields, relations, scopes, actions and rules. A thin controller binds that resource to a route, and one call to Rest::resource() registers a fixed set of endpoints. From the official introduction, for a products resource you get:
GET /api/products: describe the resource for the current user (details)POST /api/products/search: filter, sort, include, aggregate and paginatePOST /api/products/mutate: create and update rows and relations in one transactionPOST /api/products/actions/{action}: run a declared actionPOST /api/products/metrics/{metric}: compute a declared metricDELETE /api/products: delete rows (plus opt-in restore and force delete routes)
A column that the resource does not declare simply does not exist for clients, and every endpoint goes through your Laravel policies.
Requirements and installation
According to Laravel News’ coverage of 3.0, version 3 requires PHP 8.3+ and Laravel 13.30+. Applications still on Laravel 12 should stay on 2.x.
composer require lomkit/laravel-rest-api:^3.0
# Optional: publish config/rest.php
php artisan vendor:publish --tag=rest-config
# Optional: OpenAPI docs through Scramble
composer require dedoc/scramble:^0.13.48
Generate your first resource
The fastest route is the quick start command. It asks which models to expose and writes a resource, a controller, a starter policy and a route for each:
php artisan rest:quick-start
# Non-interactive, e.g. in CI or scripts
php artisan rest:quick-start --no-interaction --model=Product --model=Category --install-api
The generated route looks like this, behind Sanctum by default:
// routes/api.php
use App\Rest\Controllers\ProductsController;
use Lomkit\Rest\Facades\Rest;
Rest::resource('products', ProductsController::class)
->middleware('auth:sanctum');
Important: the generated policies allow every ability to any signed-in user. The docs are explicit that you must edit them before production.
A resource with typed fields
In 2.x a resource listed column names. In 3.x it declares typed fields, and that one declaration drives filters, validation, response types and API docs:
namespace App\Rest\Resources;
use App\Models\Product;
use Lomkit\Rest\Fields\ID;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
use Lomkit\Rest\Relations\BelongsTo;
class ProductResource extends Resource
{
public static string $model = Product::class;
public function fields(): array
{
return [
ID::make('id'),
Text::make('name')->rules('max:120')->creationRules('required'),
Integer::make('price')->rules('min:0'),
];
}
public function relations(): array
{
return [
BelongsTo::make('category', CategoryResource::class),
];
}
}
Filters only accept operators that make sense for the field type (anything else is a 422), numbers come back as numbers, and validation errors point at exact keys like mutate.0.attributes.price.
Search and mutate requests
Searches are POST requests with a JSON body. This one, from the quick start guide, finds products under 100:
POST /api/products/search
{
"search": {
"filters": [
{"field": "price", "operator": "<", "value": 100}
],
"selects": [
{"field": "id"}, {"field": "name"}, {"field": "price"}
]
}
}
The response is paginated (50 rows per page unless you send limit). Creating a product and attaching it to a category happens in one mutate call and one transaction:
POST /api/products/mutate
{
"mutate": [
{
"operation": "create",
"attributes": {"name": "Floor lamp", "price": 129},
"relations": {
"category": {"operation": "attach", "key": 1}
}
}
]
}
Confirm dangerous writes with a 428
New in 3.0, a resource or action can require confirmation before it runs. The endpoint answers 428 Precondition Required with your message and a signed token; the client repeats the same request with that token and it runs.
use Illuminate\Database\Eloquent\Collection;
use Lomkit\Rest\Confirmation\Confirmation;
public function destroyConfirmation(Collection $models): ?Confirmation
{
return Confirmation::make("Delete {$models->count()} products?")
->data(['names' => $models->pluck('name')->all()]);
}
The token holds no server state; it is signed with APP_KEY and expires after 300 seconds by default. If the number of affected rows changes, the client gets a fresh 428 instead of deleting more than the user agreed to.
What is new in 3.3 (12 Nov 2026)
The 3.3.0 release notes add typed input readers for actions and instructions, named like Laravel’s Request helpers:
$fields->string('reason'); // ?string
$fields->integer('quantity'); // ?int
$fields->date('starts_on'); // ?CarbonImmutable
$fields->enum('status', Status::class); // ?Status
A value of the wrong type throws InvalidFieldException. 3.3 also adds a checked sync for HasMany and MorphMany relations via ->syncable(): the package checks that your list accounts for every child the user can see, and returns a 409 instead of deleting rows the client never loaded.
What is new in 3.4 (13 Nov 2026)
3.4.0 lets you list actions and metrics by class name, with configuration in properties, so the container builds them only when used and their constructors can inject services:
public function actions(): array
{
return [CloseTickets::class];
}
class CloseTickets extends Action
{
public ?string $name = 'Close';
public array $middleware = ['throttle:tickets'];
public bool $destructive = true;
public function __construct(private readonly TicketMailer $mailer) {}
}
Typed readers also gain required: true, which returns a non-null type for PHPStan and throws if the value is null:
$fields->date('last_day', required: true); // CarbonImmutable
The release also recommends adding Laravel’s WithCachedRoutes trait to your base test case so resources are not rebuilt for every test.
Checks to run in CI
php artisan rest:checkcatches missing policies and perimeters, and in 3.4 it builds every action and metric and reports declaration errors.- After changing an action’s key or middleware, run
php artisan route:cacheagain. - If you upgrade from 2.x, read the upgrade guide section by section; 3.0 changed declarations, payloads and responses.
Takeaway
Laravel Rest Api 3 is a good fit when your SPA, mobile app or admin panel mostly reads and writes Eloquent models: one typed resource gives you search, nested writes, policies, metrics and OpenAPI docs. It is not the right tool if your clients expect JSON:API or GraphQL, or if you rely on URL-based HTTP caching, since searches are POST requests. Start with rest:quick-start, tighten the generated policies, and add rest:check to CI.
