Declare a resource. Get the whole API.
Laravel Rest Api turns your Eloquent models into a complete JSON API: search with filters and nested includes, nested writes in one transaction, actions and metrics, with your policies applied at every level. One resource, one controller, one route line.
composer require lomkit/laravel-rest-apiPHP 8.3+ · Laravel 13.30+ · MIT license
class ProductResource extends Resource
{
public static string $model = Product::class;
public function fields(): array
{
return [
ID::make('id'),
Integer::make('category_id'),
Text::make('name'),
Integer::make('price'),
];
}
public function relations(): array
{
return [
BelongsTo::make('category', CategoryResource::class),
];
}
}
Rest::resource('products', ProductsController::class);
{
"search": {
"filters": [
{ "field": "price", "operator": "<", "value": 100 }
],
"includes": [
{ "relation": "category" }
]
}
}
{
"current_page": 1,
"data": [
{
"id": 1, "category_id": 1, "name": "Desk lamp", "price": 39,
"category": { "id": 1, "name": "Lighting" }
}
],
"from": 1, "last_page": 1, "per_page": 50, "to": 1, "total": 1,
"meta": {}
}
Filters on declared fields, includes of declared relations, pagination. Nothing else gets through.
An API in minutes, not sprints
Run rest:quick-start, or write a resource by hand: typed fields, relations, scopes, instructions, actions and metrics. One route line registers its endpoints.
- Typed fields: ID, Text, Number, Integer, Boolean, Date, DateTime, Enum and Json.
- Generators for resources, controllers, policies, actions, instructions, metrics and responses.
- What you do not declare is not exposed.
use Lomkit\Rest\Facades\Rest;
Rest::resource('products', ProductsController::class);
$ php artisan route:list --path=api/products
| GET|HEAD | api/products | products.details |
| POST | api/products/search | products.search |
| POST | api/products/mutate | products.mutate |
| POST | api/products/actions/{action} | products.operate |
| POST | api/products/metrics/{metric} | products.metric |
| DELETE | api/products | products.destroy |
Showing [6] routes. ->withSoftDeletes() adds restore and force delete.
Built for the frontend you actually ship
Your interface asks the API what the user may do, instead of guessing. Buttons, forms and bulk actions follow your policies, and forms validate as the user types.
- Details per user: fields, operators, relations, actions and authorizations, only the parts you ask for.
- Per-row gates with the messages of your policies.
- Laravel Precognition on search, mutate and actions.
Products
New product- EditDelete
Desk lamp
Lighting · 39
- EditDelete
Floor lamp
Lighting · 129
This product has orders.
The unit price field must be at least 0.
"authorizations": {
"viewAny": { "allowed": true, "message": null },
"create": { "allowed": true, "message": null }
}
The details endpoint answers for the signed-in user: show the button, or not.
Documented and typed, out of the box
Scramble documents every endpoint from your declarations: one schema per filterable field with its operators and values, the records, the actions and the metrics. The schemas are tested against real responses.
- An OpenAPI document your clients can generate code from.
- Typed PHP all the way: Larastan level 10.
- A Laravel Boost skill that knows the package.
- Show
- GETGet Show details
- POSTSearch Show
- POSTMutate Show
- POSTReschedule Shows
- POSTShows Per Month
- DELETEDelete Show
- POSTRestore Show
{
"type": "object",
"properties": {
"field": { "type": "string", "const": "status" },
"operator": { "type": "string", "enum": ["=", "!="] },
"value": {
"type": "string",
"enum": ["planned", "on_sale", "sold_out"]
},
"type": { "type": "string", "enum": ["and", "or"] }
},
"required": ["field", "value"],
"additionalProperties": false
}
OpenAPI
Through Scramble, with a schema per field, operator and relation.
Level 10
Larastan at its highest level on the package source.
Boost
A Laravel Boost skill ships with the package.
Grows with your app
Start with SQL, add a search engine when the data grows. Queue the heavy actions, chart the numbers, export what a user sees, and keep one set of rules for all of it.
Explore search enginesStart with LIKE, move to the full-text index of your database, then to a Scout engine, behind the same search.text key.
Each metric gets its route, reads the perimeter and the search of the request, and can stay cached.
$rows = Rest::search(OrderResource::class)
->as($request->user())
->payload($request->input('search', []))
->lazy();
foreach ($rows as $row) {
fputcsv($file, [$row['id'], $row['reference']]);
}
Replay a client search outside HTTP, as a given user, with the same perimeter and policies. Rows stream by chunks.
Laravel queue attributes, batch progress on its own route, the worker runs as the user who asked.
class ExportOrders extends Action
implements ShouldQueue
Every mutation runs in one transaction, which can retry on deadlocks. Checks run in one batch before the first write, and decisions are cached.
Before / after
Less code, and it answers more requests
A typical hand-written controller for products, which also needs two form requests, a JSON resource and its routes, next to the resource that replaces them, plus a controller that names it and one route line.
class ProductController extends Controller
{
public function index(Request $request)
{
Gate::authorize('viewAny', Product::class);
$validated = $request->validate([
'max_price' => ['nullable', 'integer'],
'with_category' => ['boolean'],
'per_page' => ['in:10,25,50'],
]);
$products = Product::query()
->when($validated['max_price'] ?? null, fn ($query, $price) => $query->where('price', '<', $price))
->when($validated['with_category'] ?? false, fn ($query) => $query->with('category'))
->latest('id')
->paginate($validated['per_page'] ?? 50);
return ProductResource::collection($products);
}
public function store(StoreProductRequest $request)
{
Gate::authorize('create', Product::class);
return new ProductResource(Product::create($request->validated()));
}
public function update(UpdateProductRequest $request, Product $product)
{
Gate::authorize('update', $product);
$product->update($request->validated());
return new ProductResource($product);
}
public function destroy(Product $product)
{
Gate::authorize('delete', $product);
$product->delete();
return response()->noContent();
}
}
class ProductResource extends Resource
{
public static string $model = Product::class;
public function fields(): array
{
return [
ID::make('id'),
Integer::make('category_id'),
Text::make('name')->rules('max:255')->creationRules('required'),
Integer::make('price')->rules('min:0'),
];
}
public function relations(): array
{
return [
BelongsTo::make('category', CategoryResource::class),
];
}
}
| Hand-written | Resource | |
|---|---|---|
| Filters | One, written by hand: price below a value. | Every declared field, with the operators of its type, nested groups and relation paths. |
| Includes | The category, on or off. | Every declared relation, nested, filtered, sorted and limited per parent row. |
| Writes | One product per request. | Batches with nested relations, in one transaction. |
| Authorization | One Gate call per method. | Policies on every resource the request reaches, row checks before the first write. |
| Documentation | To write and keep in sync. | OpenAPI generated from the declarations. |
A hand-written controller is the right tool for a one-off endpoint. When the frontend asks for one more filter, one more include or one more bulk edit, the resource already answers.
Your next API is one resource away
Install the package, run the quick start, and send your first search in a few minutes.
php artisan rest:quick-start