---
url: /laravel-api-generator/guide/upgrading.md
---
# Upgrading to 4.0

4.0 is a major release. This page lists what changes when a project moves from 3.x.

## Laravel 12 or 13

4.0 needs Laravel 12 or 13. Composer picks the right line on its own: on a Laravel 10 or 11 project, the usual install command resolves to 3.15, which carries every generator fix released before 4.0.

```bash
composer require --dev nameless/laravel-api-generator
```

Once the project runs Laravel 12 or 13, update the package:

```bash
composer require --dev nameless/laravel-api-generator:^4.0
```

The MCP server still needs `laravel/mcp`, which asks for Laravel 12.41 or later on the 12.x line.

## Regenerate your entities

Updating the package changes no file in your project. The changes below reach an entity when you regenerate it. Files you edited by hand since the generator wrote them are kept and named in a `modified_file_kept` warning. Port the changes described here into them, or regenerate them with `--force` and redo your edits, since the new DTO only accepts the new requests.

## Controllers and routes

The controllers receive the model through route model binding, so `show(Post $post)` replaces `show(int|string $id)` and its `find()` call. The variable is named after the parameter that `Route::apiResource()` registers, usually the lowercase singular (`$post`, `$blogpost`) and sometimes not (`$medium` for `Media`), because binding matches by name only.

With `--soft-deletes`, the restore and force-delete routes take the same parameter and call `withTrashed()`, so a soft deleted model still resolves:

```
POST   /api/posts/{post}/restore
DELETE /api/posts/{post}/force-delete
```

Regenerating the entity replaces the `{id}` routes that 3.x wrote. `restore` now returns the restored resource instead of a message, and the delete actions answer `204 No Content`.

## Policies

Every controller action now asks the entity's policy first, through `Gate::authorize()`. The generated policies accept guests (`?User $user`) and return `true`, so an API without `--auth` stays public until you tighten a policy. Returning `false` from `update()`, for instance, makes `PATCH /api/posts/1` answer 403.

A policy generated by 3.x types its first parameter as `User $user`, which Laravel reads as "no guests". On an API without authentication, every request would get a 403. Regenerate the policy, or make that parameter nullable.

`store` and `update` validate the request before the policy runs, so a client the policy refuses still gets a 422 when its data is invalid. If that matters for your API, move the check into the `authorize()` method of the request.

## Store and Update requests

`PostRequest` becomes `StorePostRequest` and `UpdatePostRequest`. The update request prefixes every rule with `sometimes`, so a PATCH may send only the fields it changes, and its unique rules still ignore the current row. Nullable fields validate with `nullable` instead of `sometimes`, which accepts an explicit `null`.

After regenerating, the old `PostRequest.php` is no longer used. The generation warns about it (`legacy_request`) until you move your own rules into the new requests and delete it. `delete:fullapi` removes it along with the rest.

## DTOs and partial updates

The DTO is built from `$request->validated()` and remembers which fields the request sent. Its properties are nullable with a `null` default, and `toArray()` returns the sent fields only. The services save `$dto->toArray()`, so a PATCH leaves the other columns as they are. A DTO you build yourself, as the generated unit tests do, still saves every property.

A field named `provided` is refused, since the DTO keeps its list of sent fields under that name.

## Paginated index

The index is paginated, filterable and sortable without any extra package. `GET /api/posts?filter[status]=draft&sort=-created_at&page=2&per_page=20` answers `data`, `links` and `meta`, and the service method behind it, `getAll()`, becomes `paginate(array $query)`. Clients that read the whole list in one call now get 15 items per page, and the plain filters of 3.x (`?status=draft`) become `filter[status]=draft`. With `--query-builder`, filters match exact values instead of Spatie's default partial match.

The page size comes from a new config file, read when you generate. Publish it with `php artisan vendor:publish --tag=api-generator-config` to change the default of 15 or the cap of 100.

## Models and enums

On Laravel 13, the model declares its key and its fillable columns with class attributes. The generator reads the Laravel version when it runs, so a Laravel 12 project keeps the `$primaryKey`, `$keyType`, `$incrementing` and `$fillable` properties:

```php
#[Table(key: 'code', keyType: 'string', incrementing: false)]
#[Fillable(['code', 'name'])]
class Country extends Model
```

`#[Table]` only appears with a custom primary key. On both versions, the casts move from the `$casts` property to a `casts()` method, and `--add-fields` adds to whichever one the model has.

Enums are named after the entity and the field: `status` on `Post` gives `App\Enums\PostStatus` instead of `App\Enums\Status`, so two entities with a `status` field no longer write to the same enum. The enum written by 3.x stays in place. The generation names it in a `legacy_enum` warning, and you delete it once no code uses it. `delete:fullapi` removes the enums of the entity it deletes.

## Generated code

The generated files pass `pint --test` with the Laravel preset: sorted imports and short class names, routes included. `routes/api.php` imports each controller, and regenerating an entity rewrites the fully qualified references written by 3.x.

A few behaviors change along the way:

* `json` fields validate with `array` instead of `json`. Clients send an object or a list, and a JSON-encoded string gets a 422.
* The DTO casts string fields too, so a date or a code sent as a number, such as `20250101`, no longer ends in a 500.
* The resource of an entity with a custom primary key no longer returns an `id` key, which was always `null`.
* The columns of a `morphTo` relation are nullable (`nullableMorphs`), since the generated requests do not fill them. Attach the owner through the relation, for example `$post->comments()->create([...])`.

## Published stubs

Stubs published under `stubs/vendor/laravel-api-generator` keep taking precedence, so compare them with the new ones. `php artisan api-generator:validate-stubs` points at what a 3.x stub misses:

::: v-pre

* `request.stub` is no longer read. Copy your changes into `request.store.stub` and `request.update.stub`, then delete it.
* `controller.stub` and `controller.query-builder.stub` need `{{routeParameter}}`, the variable the route binds.
* `dto.stub` needs `{{attributesFromValidated}}` in place of `{{attributesFromRequest}}`.
* `service.stub` and `service.query-builder.stub` must save `$dto->toArray()` instead of `get_object_vars($dto)`, or a PATCH would clear the fields it leaves out.
* `policy.stub` needs `{{modelVariable}}`.
* `service.stub` and `service.query-builder.stub` need `{{allowedFilters}}`, `{{allowedSorts}}`, `{{perPage}}` and `{{maxPerPage}}`.
* `test.unit.stub` and `test.unit.pest.stub` must call `paginate()`, since `getAll()` no longer exists.
* `model.stub` needs `{{members}}`, which holds the traits, the properties, `casts()` and the relations, and `{{classAttributes}}` right above `class`, where the Laravel 13 attributes go. A 3.x model stub still gets `{{traits}}`, `{{fillable}}` and `{{relationships}}`, so it keeps generating a working model, with properties.
* `migrations.stub` needs `{{columns}}` for the whole table body. The 3.x placeholders are still filled.
* `request.store.stub` and `request.update.stub` need `{{imports}}` after the `FormRequest` import, for `Rule` and the enums their rules use.

The feature test stubs gain optional placeholders for the new cases: `{{patchFields}}`, `{{patchAssertion}}`, `{{patchedColumns}}`, `{{softDeleteTests}}`, `{{filterField}}`, `{{primaryKey}}` and `{{sortAssertion}}`.
:::

## Removed

* **`config/laravel-api-generator.php`**. The generator never loaded it, so its paths, namespaces and field types had no effect. If you copied it into your project, delete it.
* **`generateFromJson()` and `deleteCompleteApi()`** on `ApiGenerationServiceInterface`, deprecated since 3.8. Generate with `php artisan make:fullapi` (a schema file, `class_data.json` or any other source) and delete with `php artisan delete:fullapi`.
