Getting Started

Quick start

Expose your first models with rest:quick-start, then send your first search and mutate requests.

Expose your models

Run the quick start command in your application:

php artisan rest:quick-start

The command asks Which models should the API expose? and lists the models of app/Models. Pick one or more. For each model, it writes:

  • a resource in app/Rest/Resources, with one field per column of the table and the relations to the other chosen models, as rest:resource --generate does;
  • a controller in app/Rest/Controllers;
  • a policy in app/Policies that allows every ability to a signed-in user, when Gate finds no policy for the model;
  • a route in routes/api.php, behind the authentication middleware of the guard.

It also creates the base classes app/Rest/Resources/Resource.php and app/Rest/Controllers/Controller.php when they are missing. An existing class is never changed: the command reports it as kept, so a second run changes nothing.

For a Product model, the route looks like this:

routes/api.php
use App\Rest\Controllers\ProductsController;
use Lomkit\Rest\Facades\Rest;

Rest::resource('products', ProductsController::class)
    ->middleware('auth:sanctum');
The generated policies let every signed-in user do everything. Edit them before production. See Policies.

Without a terminal

In CI, in a script, or with --no-interaction, name each model with --model. The command never answers its own questions, so pass --install-api too when routes/api.php or Laravel Sanctum is missing:

php artisan rest:quick-start --no-interaction --model=Product --model=Category --install-api

Choosing the guard

The routes use the sanctum guard by default. Pass --guard to use another one:

  • sanctum uses Laravel Sanctum.
  • api uses Laravel Passport, when config/auth.php does not define it or defines it with the passport driver.
  • Any other guard that config/auth.php defines is used as it is, for example --guard=web.
php artisan rest:quick-start --model=Product --guard=api --install-api

When routes/api.php does not exist, or when the package of the guard is missing, the command asks before it runs php artisan install:api (Laravel Sanctum) or php artisan install:api --passport (Laravel Passport). --install-api runs it without asking.

When such an install is needed, --guard is not passed and a terminal can answer, the command first asks Which guard should protect the API routes?, with sanctum as the default. Without a terminal, the guard is sanctum.

Laravel Passport needs an api guard with the passport driver in config/auth.php. The command never edits that file: add the guard yourself.

What the command checks

Before it writes anything, the command stops with exit code 1 when:

  • the routes of the application are cached: run php artisan route:clear first;
  • a model does not exist or has no table;
  • two chosen models would get the same resource name, such as App\Models\Note and App\Models\Archive\Note;
  • an existing route already uses the URIs or the names of the new routes, such as Route::apiResource('products', ...);
  • Gate would not find the policy that it writes, for example for a model outside the namespace of the application. Register a policy with Gate::policy(), then run the command again.

After writing, it checks the result in a new PHP process: each route is behind an authentication middleware whose guard exists, and Gate finds a policy for each model.

  checks (fresh process) ... 6 routes authenticated, 1 policy found

It does not check the perimeters of the resources or the row checks of the actions: run php artisan rest:check for these, see Perimeter.

A relation to a model that you did not choose is skipped and reported, with the option to add:

  Skipped: Post.author: no resource for App\Models\User. Rerun with --model=User to expose users.

Authenticate

The command ends with the next steps: a command that creates a token for your first user, then a curl request to the search route of the first model. The request goes to the app.url of your configuration, http://localhost when it is empty. When the command wrote policies, a reminder that they allow every ability follows. With Laravel Sanctum:

  Next:
    php artisan tinker --execute="echo App\Models\User::first()->createToken('cli')->plainTextToken;"
    curl -X POST http://localhost/api/products/search -H "Authorization: Bearer <token>" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"search":{"limit":10}}'
  ProductPolicy lets signed-in users do everything. Edit app/Policies before production.
  • With Laravel Passport, the token command reads accessToken instead of plainTextToken.
  • With another guard, the command prints Authenticate with the web guard to call the API. (for --guard=web) instead of a token command, and the curl request has no Authorization header.
  • The user model needs the HasApiTokens trait of Laravel Sanctum or Laravel Passport. The command never edits the user model: it warns when the trait is missing.

Assume the generated ProductResource declares id, name and price, and the table holds a desk lamp at 39 and a floor lamp at 129. Search the products cheaper than 100:

POST /api/products/search
Authorization: Bearer <token>
Accept: application/json
{
  "search": {
    "filters": [
      {"field": "price", "operator": "<", "value": 100}
    ],
    "selects": [
      {"field": "id"},
      {"field": "name"},
      {"field": "price"}
    ]
  }
}
{
  "current_page": 1,
  "data": [
    {"id": 1, "name": "Desk lamp", "price": 39}
  ],
  "from": 1,
  "last_page": 1,
  "per_page": 50,
  "to": 1,
  "total": 1,
  "meta": {}
}
  • A page holds 50 rows unless the request sends limit, one of the resource's limits().
  • Without selects, each row holds every selectable field of the resource.
  • A filter on a field that the resource does not declare gives a 422.

The search endpoint also takes sorts, includes, aggregates, scopes and a text search. See Search.

Your first mutation

Create a product and attach it to the category 1:

POST /api/products/mutate
{
  "mutate": [
    {
      "operation": "create",
      "attributes": {"name": "Floor lamp", "price": 129},
      "relations": {
        "category": {"operation": "attach", "key": 1}
      }
    }
  ]
}

The response gives one entry for each entry of mutate, with the key of the created row:

{
  "operations": [
    {"operation": "create", "key": 2, "relations": {"category": {"operation": "attach", "key": 1}}}
  ]
}

The field rules validate each attribute. The generator marks a column that is not nullable and has no default with creationRules('required'). Without such a name, the request gives a 422 and writes nothing:

{
  "message": "The mutate.0.attributes.name field is required.",
  "errors": {
    "mutate.0.attributes.name": ["The mutate.0.attributes.name field is required."]
  }
}

See Mutate for updates, nested writes and the response block that returns the written rows.

Next steps

  • Resources: write and register a resource by hand.
  • Fields: choose what clients read, filter, sort and write.
  • Perimeter: decide which rows exist for each client.
  • Configuration: change the defaults of the package.