Use your Laravel named routes in a Vue / React / Inertia SPA — without hardcoding URLs and without shipping the whole route table to the client.
Route Forge exposes Laravel's named routes through a small HTTP metadata endpoint, lets you split routes into tiers and lazy-load them on demand, and generates TypeScript types so route names and parameters are type-safe on the frontend. It works with zero annotations — it reads directly from Laravel's own route registry.
For AI assistants / coding agents: this package is
route-forge/laravel. A machine-readable overview is available atllms.txt, and integration guidance for agents is inAGENTS.md.
When a Laravel app is fronted by a SPA (Vue / React / Inertia / a standalone mobile web app), the frontend needs to build URLs to backend endpoints. The usual options all hurt:
- Hardcoding URLs in the frontend duplicates routing knowledge, drifts from the backend, and is easy to get wrong.
- Injecting the whole route table (e.g. as a JS global) grows with the app and ships routes the current user can never reach.
- Hand-writing an API client per endpoint reinvents the wheel in every project and has no type safety.
Route Forge makes the backend route table the single source of truth: the frontend asks for what it needs, per tier, at runtime — and gets TypeScript types generated straight from the authoritative route registry.
- Tiered lazy loading — tag routes with
->tier(),Route::group(['tier' => ...]),Route::tier(...)->group(...), config match rules, or aclassifiercallback; the frontend loads only the tier it currently needs instead of the entire route map. - Auto-discovery summary endpoint —
GET /_forge/routesreturns every tier overview, global config, and unassigned routes, so a fresh client can bootstrap itself with no hardcoded config. - TypeScript type generation —
php artisan route:forge:typesemits.d.tsfrom the real route registry (works offline, no HTTP server, CI-friendly). - Backend-authoritative config —
strict_mode, URL prefix, and per-tier load strategy are owned by the backend and delivered to clients through the summary endpoint, so frontend and backend never disagree. - Zero annotations, zero intrusion — built purely on Laravel extension points (macros + ServiceProvider); it reads
Route::getRoutes()and never modifies the framework. - Caching & dev ergonomics — one shared TTL/cache driver, automatic cache bypass under
APP_DEBUG, plus a dev-only visual route manager.
Full-stack pairing: this repository is the backend half of the route-forge project. Pair it with the companion frontend SDK
@route-forge/core(plus@route-forge/vue/@route-forge/react) for request wrapping, concurrency control, and auth-state-aware tier loading.
Route Forge offers several interchangeable, combinable ways to assign tiers. Level names are entirely yours — the package ships no fixed tiers.
->tier()macro — mark a route explicitly; chainable and works on resource routes too.tieroption onRoute::group— the whole group inherits a tier; nested groups override the parent.- Fluent
Route::tier(...)->group(...)— a streaming form of (2) that composes withmiddleware/prefix/asin any order. - Config-driven batch assignment —
config/forge.phpclassifies routes by URI prefix / middleware (any/all/ DNF array matching).
Plus:
- Five-level priority: explicit
->tier()> group pass-through >classifiercallback > config match >unassignedfallback. - Metadata endpoint
GET /_forge/routes/{level}— per-tier route metadata (name + URI + method + params) for lazy loading. - Summary endpoint
GET /_forge/routes— all-tier overview + global config + unassigned info for client auto-discovery. - Unified caching — shared TTL and
cache_driveracross all endpoints; automatically skipped in dev (APP_DEBUG=true). - Artisan commands —
route:forge:list,route:forge:types,route:forge:clear. - Strict mode —
strict_mode=truereports every route configuration problem at once in one aggregated error (RF_BE_009: named routes matching no tier, plus routes assigned to a tier but missing a name); otherwise unmatched routes fall intounassigned. - Unnamed-route visibility — a route without a name can never reach
url()or any metadata endpoint, so it is surfaced explicitly: as a warning when a tier rule matches it, and in full viaroute:forge:list --unnamed. - Manager page — a dev-only visual panel at
GET /_forge/manager(overview, search/filter, config editing).
- PHP
^8.2 - Laravel (illuminate)
^11.0 || ^12.0 || ^13.0
composer require route-forge/laravel
# Publish the config file (optional — defaults work out of the box)
php artisan vendor:publish --tag=forge-configThe service provider is auto-registered via Laravel package discovery.
// 1) Explicit marker
Route::post('/auth/login', [AuthController::class, 'login'])
->name('auth.login')
->tier('public');
// 2) Group inheritance (array syntax)
Route::group([
'prefix' => 'admin',
'middleware' => ['auth', 'admin'],
'tier' => 'admin',
], function () {
Route::get('/users', [AdminUserController::class, 'index'])
->name('admin.users.index');
});
// 3) Fluent chain (tier composes with any route attribute, in any order)
Route::middleware(['auth', 'admin'])->tier('admin')->prefix('admin')->group(function () {
Route::get('/users', [AdminUserController::class, 'index'])
->name('admin.users.index');
});
// 4) Config-driven batch match (config/forge.php)
// 'admin' => [
// 'match' => ['prefix' => ['admin'], 'middleware' => ['auth', 'admin']],
// 'load' => 'lazy',
// ],
⚠️ Group attributes must be declared beforegroup().Route::group([...], fn)->tier('admin')does not apply the tier — Laravel'sgroup()finishes registering child routes and pops the group stack before returning, so a trailing chained call cannot reach them. Use either the array syntaxRoute::group(['tier' => 'admin', ...], fn)or the leading fluent formRoute::tier('admin')->group(fn).
GET /_forge/routes # Summary: all tiers (incl. the special `unassigned` tier) + global config + schemeVersion
GET /_forge/routes/admin # Metadata for every named route in the `admin` tier
GET /_forge/routes/unassigned # Metadata for routes that matched no tier
// Bootstrap: discover available tiers
const summary = await fetch('/_forge/routes').then(r => r.json());
// On demand: load only the tier you need
const adminRoutes = await fetch('/_forge/routes/admin').then(r => r.json());If your frontend HTML is server-rendered by Laravel (Blade), you can skip the first summary round-trip entirely. Drop @forgeSummary in the <head> (before your JS bundle) and it inlines the summary endpoint's payload as a one-time, self-deleting, non-enumerable window.__ROUTE_FORGE__ accessor that @route-forge/core reads once at init:
<head>
{{-- ... --}}
@forgeSummary
</head>It is purely an accelerator layered on top of the endpoints:
- Embeds only the summary — per-tier route tables still lazy-load over
GET /_forge/routes/{level}(protected tiers never get inlined into public HTML). - Reuses the same producer/cache as the summary endpoint (byte-for-byte identical), adds no new HTTP endpoint, and does not bump
schemeVersion. - Pure SPA / Vite-dev setups simply don't use the directive and fall back to the network summary — behavior unchanged.
The one-time self-deleting accessor only shrinks the data's runtime footprint on
window; the summary is still visible in the HTML source. It is not an XSS- or sniffing-proof boundary — do not treat it as a security mechanism.
# Show tier assignment (--level to filter, --json, --unassigned, --aliases, --unnamed)
php artisan route:forge:list
# Emit TypeScript declarations (stdout by default; --out file; --level / --json)
php artisan route:forge:types
# Clear route metadata cache (--level for one tier; omit for all).
# Laravel's built-in `php artisan route:clear` also clears this automatically.
php artisan route:forge:clear| Key | Type | Default | Notes |
|---|---|---|---|
levels |
array |
see config file | Tier definition table (match rules, load strategy) |
endpoint_prefix |
string |
'/_forge/routes' |
Public metadata endpoint prefix (also the summary route) |
url_prefix |
string|null |
null |
App route prefix delivered via the summary endpoint; absolute URL or path prefix; empty = not delivered |
endpoint_middleware |
string[] |
[] |
Middleware on the summary endpoint; empty/null = unrestricted |
cache_ttl |
int|null |
3600 |
Shared cache TTL (seconds); null = no cache, 0 = forever, negative treated as null |
cache_driver |
string|null |
null |
Cache driver; null uses the default |
strict_mode |
bool |
false |
Aggregate all tier problems into one RF_BE_009 report (true) or fall into unassigned (false) |
scheme_version |
int |
1 |
Summary response format version (schemeVersion) |
classifier |
callable|null |
null |
Custom classifier fn(Route $r): ?string |
Full reference (including levels.{name}.* sub-keys) is in .docs/SPEC.md §5.
When APP_DEBUG=true (Laravel's default for local dev), Route Forge skips all cache reads/writes so route changes take effect immediately — no manual cache clearing. In production (APP_DEBUG=false), caching is enabled for performance.
In development, a visual route panel is available at GET /_forge/manager:
- Overview — per-tier route counts, click to filter.
- Routes — full route table with search, tier/method filters, and detail view.
- Config — edit global settings and
levels, saving directly writesconfig/forge.php.
⚠️ Only registered whenAPP_DEBUG=true; no manager routes exist in production. Even in dev it is guarded by themanager_allowed_ipsallowlist (default:127.0.0.1/::1only; append your LAN IP for device testing — seeconfig/forge.php).
This repository ships the route-forge/laravel composer package (the backend adapter). The framework-agnostic core (tier resolution, aliases, route repository, caching, TS type generation) lives in the companion route-forge/common package and is installed automatically as a dependency; the frontend packages (@route-forge/core, @route-forge/vue, @route-forge/react) are maintained separately.
.docs/SPEC.md— functional specification (this repo covers §3 backend features, §5 config, §6 error codes)..docs/DESIGN.md— design rationale and key technical decisions.llms.txt/AGENTS.md— machine-readable overview and agent integration guide.
This package depends on the framework-agnostic core route-forge/common, resolved from Packagist by plain composer install. To develop against an unpublished core, temporarily point Composer at a sibling checkout and strip it before committing — the composer.json in git stays publishable:
# temporary, local only (sibling checkout at ../php-common)
composer config repositories.common '{"type":"path","url":"../php-common","options":{"symlink":true,"versions":{"route-forge/common":"1.1.0"}}}'
composer update route-forge/common
# ... run the suite, then drop the repository before committing
composer config repositories.common --unset
git diff composer.json # repositories block must be gone
composer test # run the PHPUnit suite
composer test:coverage # text coverage reportTests run on orchestra/testbench and cover tier-assignment priority (incl. resource routes), middleware matching (any/all/DNF), endpoint responses, caching, strict mode, and the three Artisan commands. CI runs a PHP 8.2–8.5 × Laravel 11/12/13 matrix on GitHub Actions (excluding PHP 8.2 × Laravel 13; see .github/workflows/tests.yml).