6 min read

Laravel + Inertia as a Headless CMS

Running an Inertia admin panel and a JSON API off the same Laravel app — where the seams are, and how to keep the two consumers from fighting each other.

LaravelInertia.jsAPI Design
Laravel + Inertia as a Headless CMS

The setup

One Laravel app, two front doors. Editors get an Inertia + Vue admin panel with full server-side routing. The public portfolio gets plain JSON over HTTP. Same models, same validation, same authorization.

The failure mode people hit is letting one consumer's needs leak into the other's response shape.

Separate the resource layer

Inertia pages want everything eagerly: relations, computed labels, form option lists. The public API wants a lean, stable contract.

Give them different resources.

// Admin — verbose, shaped for the form
class ProjectAdminResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            ...$this->resource->toArray(),
            'tags' => TagResource::collection($this->whenLoaded('tags')),
            'available_tags' => Tag::pluck('name'),
        ];
    }
}

// Public — narrow and versioned
class ProjectResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'slug' => $this->slug,
            'title' => $this->title,
            'description' => $this->description,
            'tags' => $this->tags->pluck('name'),
        ];
    }
}

The shared toArray() is a trap — the moment an editor needs a new field, the public contract silently grows.

Route groups do the rest

Route::middleware(['auth', 'inertia'])->prefix('admin')->group(...);
Route::middleware(['throttle:api'])->prefix('api/v1')->group(...);

Different middleware stacks, different rate limits, different failure formats — Inertia wants a redirect on validation failure, the API wants a 422 with a JSON body. Laravel handles both if you keep the groups honest.

When to skip the API entirely

For content that changes a few times a month — project write-ups, articles — I stopped hitting the API at build time and moved it into flat markdown in the frontend repo. No network call, no cache invalidation, no cold-start latency.

The CMS still owns the things that genuinely need a database: contact messages, draft state, anything with an editorial workflow. Everything else is a file.