Describe a model. Get a full API.

A Laravel package that turns your Eloquent models into a JSON API. List the fields and relations clients may use, add one route line, and get search, writes, deletes and your own actions, validated and checked against your policies.

composer require lomkit/laravel-rest-api

PHP 8.3+ · Laravel 13.30+ · MIT license

app/Rest/Resources/ProductResource.php
excerpt
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')->creationRules('required'),
            Integer::make('price'),
        ];
    }

    public function relations(): array
    {
        return [
            BelongsTo::make('category', CategoryResource::class),
        ];
    }
}
routes/api.php
Rest::resource('products', ProductsController::class);

Routes it registers

  • GET/api/products
  • POST/api/products/search200
  • POST/api/products/mutate
  • POST/api/products/actions/{action}
  • POST/api/products/metrics/{metric}
  • DELETE/api/products

POST/api/products/search

{
  "search": {
    "filters": [
      {"field": "price", "operator": "<", "value": 100}
    ],
    "includes": [{"relation": "category"}]
  }
}

Response200 OK

{
  "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": {}
}

price and category are declared, so clients can filter on one and include the other.

Try a search

Clients ask for exactly the rows they need

Filters, sorts, related rows and per-row permissions travel in one JSON body. Pick options below: the request changes, and so does the answer the package gives.

Options

POST/api/products/search
{
  "search": {
    "filters": [
      {"field": "price", "operator": "<", "value": 100}
    ],
    "includes": [{"relation": "category"}]
  }
}
Response200 OK
example data
{
  "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": {}
}

Four products in two categories. Without a sort, rows come by key, highest first. A page holds 50 rows unless the request sends a limit the resource allows.

Beyond search

Every resource gets the same endpoints

Write rows with their relations, run your own actions, compute metrics, describe the resource, delete rows, and restore them when the model uses soft deletes. Each one checks the same declarations and the same policies.

Count and total related rows

count, exists, min, max, avg and sum over a relation, computed in SQL next to each row.

Read the docs
CategoryResource.php
excerpt
public function relations(): array
{
    return [
        HasMany::make('products', ProductResource::class),
    ];
}
POST/api/categories/search
{
  "search": {
    "aggregates": [
      {"relation": "products", "type": "count"},
      {"relation": "products", "type": "max", "field": "price"}
    ]
  }
}
Response200 OK
{
  "current_page": 1,
  "data": [
    {"id": 1, "name": "Lighting", "products_count": 2, "products_max_price": 129}
  ],
  "from": 1,
  "last_page": 1,
  "per_page": 50,
  "to": 1,
  "total": 1,
  "meta": {}
}

Safe by default

Nothing is written until every check passes

Your Laravel policies and validation rules run on every request, in a fixed order, before anything is written. Pick a request and watch where it stops.

  1. Entry checksviewAny on every resource the request reaches, before validation.
  2. ValidationDeclared fields and rules only, keys read inside the perimeter.
  3. Row checksIn the transaction, the policy of every row, before the first write.
  4. WriteEvery entry is written, or the transaction rolls back.
POST/api/products/mutate
{
  "mutate": [
    {"operation": "update", "key": 1, "attributes": {"cost": 20}}
  ]
}
Response403 Forbidden
{"message": "This action is unauthorized."}

The policy refuses viewAny: the request stops before validation, even with an invalid payload, and before any query.

For your frontend

Your interface asks the API what it may do

The details endpoint describes a resource for the signed-in user: fields, operators, rules, page sizes, actions and permissions. Build lists and forms from it instead of copying your rules into JavaScript.

  • One request per screen: a list, a form or an action dialog asks only for the parts it shows. Read more
  • Each row of a search can say whether the user may update or delete it, with the message of your policy. Read more
  • Turn on Laravel Precognition and forms validate as the user types: the same checks, nothing written. Read more

Artworks

Medium
The operators of the filter, from the details: =!=likenot likeilikenot ilikeinnot in

Point at a part of this screen to see the key of the details it comes from.

GET/api/artworks?with=fields.capabilities,fields.filter,fields.rules,fields.meta
excerpt: title
{"data": {
  "fields": [
    {
      "name": "title", "label": "Title", "type": "text",
      "nullable": false, "select": true, "sort": true,
      "filter": {"operators": [
        "=", "!=", "like", "not like", "ilike", "not ilike", "in", "not in"
      ]},
      "create": true, "update": true,
      "rules": {"all": ["string", "max:120"], "create": ["required"], "update": ["filled"]},
      "meta": {}
    }
  ],
  "limits": [10, 25, 50],
  "meta": {}
}}

Documented

The API documents itself

Your declarations already say what each endpoint accepts. The package turns them into an OpenAPI document through Scramble, and ships a skill so coding agents know the same rules.

Product

  • GETGet Product details
  • POSTSearch Product
  • POSTMutate Product
  • DELETEDelete Product

/docs/api

Search Product

POST /api/products/search · body ProductSearch · 200 ProductPaginated

search.filters: one condition per filterable field

  • id=, !=, in, not in
  • name=, !=, like, not like, ilike, not ilike, in, not in
  • price=, !=, >, >=, <, <=, between, not between, in, not in

Includes, aggregates, scopes, instructions, actions and metrics get their schemas too, with the 401, 403, 404 and 422 responses.

$ composer require dedoc/scramble:^0.13.48
$ php artisan scramble:export
Generate the OpenAPI document

A skill for coding agents

The package ships a laravel-rest-api skill for Laravel Boost: resources, payloads, limits, authorization and the conventions to keep, for the version you installed.

$ composer require laravel/boost --dev
$ php artisan boost:install
Install the skill

As your app grows

Everything else a real API needs

Your models are ready. Give them an API.

Three steps, and the rules you already wrote in Laravel.

  1. 1 Install the packagecomposer require lomkit/laravel-rest-api
  2. 2 Expose the models you pickphp artisan rest:quick-start
  3. 3 Send your first searchPOST /api/products/search

Laravel Rest Api is MIT licensed and maintained by Lomkit.