Routing Recipes
This page covers the advanced routing surface: resolution precedence, cache behavior, performance internals, and the real route patterns the framework ships with.
Route Model Binding
Route model binding is not provided by the router. Resolve models explicitly in handlers or via services/repositories:
use App\Models\User;
$router->get('/users/{slug}', function (string $slug) {
$user = User::where('slug', $slug)->firstOrFail();
return new Response(['user' => $user->toArray()]);
});
CORS
Configure CORS in config/cors.php (allowed origins, headers, methods, max age, credentials). The router handles OPTIONS preflight automatically for matched paths and returns the appropriate Allow header. For custom policies, add a CORS middleware (PSR-15 adapter or custom) via route groups. See CORS & CSRF.
Route Caching & Closures
Routes are automatically cached in production. In development the route cache uses a short TTL and auto-invalidates when route files change (including framework routes) — no CLI is required to manage it.
Closure handlers are not cached. Closures can't be serialized, so when the router sees any closure handler it skips writing the route cache (logging which routes are responsible) and resolves those routes live on every request. For cacheable, production-grade routing, use
[Controller::class, 'method'](or string) handlers rather than closures. Attribute routes are always controller handlers, so they always cache.
To force-clear the dev cache, delete storage/cache/routes_dev.php or call:
app($context, Glueful\Routing\RouteCache::class)->clear();
Not Found / Method Not Allowed
The router standardizes JSON error responses for 404 Not Found and 405 Method Not Allowed (405 responses include an Allow header). No explicit fallback route is needed.
Performance & Precedence
How the router resolves a request
The router uses several optimization techniques:
- Static route hash table — O(1) lookup for routes without parameters
- Route bucketing — dynamic routes grouped by first path segment
- Compiled patterns — regex patterns pre-compiled and cached
- Reflection caching — method/parameter reflection cached
Route precedence
The router resolves a request in three tiers:
- Static routes win first. An exact path like
/users/popularalways beats a dynamic/users/{id}, no matter which was registered first — static routes are matched from a hash table before any pattern matching. - Literal first segment beats a parameter first segment.
/users/{id}is preferred over/{resource}/{id}for a request to/users/5, independent of registration order. - Within the same first-segment group there is no specificity ranking — overlapping dynamic patterns are tried in registration order, and the first that matches wins.
Because of tier 3, when two dynamic patterns can match the same path, register the more specific one first:
// Correct: specific before generic
$router->get('/posts/{id}/edit', [PostController::class, 'edit']);
$router->get('/posts/{id}/{action}', [PostController::class, 'action']);
// Wrong: the generic pattern shadows /posts/{id}/edit, which is never reached
$router->get('/posts/{id}/{action}', [PostController::class, 'action']);
$router->get('/posts/{id}/edit', [PostController::class, 'edit']); // unreachable for /posts/5/edit
Parameters match exactly one path segment (default constraint [^/]+), so routes with different segment counts never collide.
Optimization tips
// 1. Static routes always beat dynamic ones (automatic; no ordering needed)
$router->get('/users/popular', $handler); // exact match, checked first
$router->get('/users/{id}', $handler); // pattern match, checked second
// 2. Use specific constraints to fail fast
$router->get('/users/{id}', $handler)->where('id', '\d+');
// 3. Group related routes under a shared prefix
$router->group(['prefix' => '/api/v1'], function ($router) { /* ... */ });
Benchmarks
- Static routes: O(1) hash table lookup (~0.001ms)
- Dynamic routes: optimized regex matching (~0.01ms)
- Route compilation: one-time cost, cached indefinitely
- Middleware pipeline: lazy resolution, minimal overhead
Inspecting routes programmatically
$routes = $router->getAllRoutes(); // method, path, handler, middleware, name
Framework Endpoints Reference
Glueful ships several built-in endpoint groups that demonstrate real-world routing patterns — useful as templates for your own routes.
Health monitoring (routes/health.php)
$router->group(['prefix' => '/health'], function (Router $router) {
// Main health check — high rate limit for monitoring tools
$router->get('/', [HealthController::class, 'index'])
->middleware('rate_limit:60,60'); // 60/min
// Component checks with lower limits
$router->get('/database', [HealthController::class, 'database'])
->middleware('rate_limit:30,60');
$router->get('/cache', [HealthController::class, 'cache'])
->middleware('rate_limit:30,60');
$router->get('/extensions', [HealthController::class, 'extensions'])
->middleware('rate_limit:20,60');
});
Authentication (routes/auth.php)
$router->group(['prefix' => '/auth'], function (Router $router) {
// Login with strict rate limiting
$router->post('/login', [AuthController::class, 'login'])
->middleware('rate_limit:5,60'); // 5 attempts/min
$router->post('/verify-email', [AuthController::class, 'verifyEmail']);
$router->post('/verify-otp', [AuthController::class, 'verifyOtp'])
->middleware('rate_limit:3,60'); // 3 attempts/min
$router->post('/refresh', [AuthController::class, 'refresh'])
->middleware(['auth', 'rate_limit:10,60']);
$router->post('/logout', [AuthController::class, 'logout'])
->middleware('auth');
});
Generic resource routes (routes/resource.php)
// List with pagination
$router->get('/{resource}', [ResourceController::class, 'get'])
->middleware(['auth', 'rate_limit:100,60']); // 100/min
// Single resource by UUID — higher read limit
$router->get('/{resource}/{uuid}', [ResourceController::class, 'getSingle'])
->middleware(['auth', 'rate_limit:200,60']);
// Create / update — moderate limits
$router->post('/{resource}', [ResourceController::class, 'create'])
->middleware(['auth', 'rate_limit:20,60']);
$router->put('/{resource}/{uuid}', [ResourceController::class, 'update'])
->middleware(['auth', 'rate_limit:20,60']);
// Delete — lowest limit for destructive ops
$router->delete('/{resource}/{uuid}', [ResourceController::class, 'delete'])
->middleware(['auth', 'rate_limit:10,60']);
Key patterns
- Rate limiting by sensitivity — auth endpoints very restrictive (3–5/min), health generous (60/min), reads high (100–200/min), writes moderate (10–20/min), destructive lowest (10/min).
- Security — all resource operations require
auth; destructive operations get the lowest limits; login/OTP are heavily restricted. - URL structure — grouped by feature (
/auth,/health), RESTful resource patterns (/{resource},/{resource}/{uuid}), predictable hierarchy.
Related
- Essentials → Routing — the fundamentals
- Advanced → Middleware — middleware pipeline + alias catalog
- Features → Field Selection —
?fields=/?expand=projection