Routing & Middleware

Define routes, attach middleware, and generate URLs with the Velvet router.

Category: HTTP Stack

Route definitions #

Routes live in routes/web.php (or modules can register their own during boot()). Each call returns a RouteDefinition, so you can fluently attach middleware or names:

$router->get('/blog/{slug}', [PostController::class, 'show'])
    ->middleware(['auth', 'throttle:60,1'])
    ->name('blog.show');

Key features:

  • Methods: get/post/put/delete/patch/any() (or arrays of verbs).
  • Parameters: {slug} (single segment), {slug?} (optional), {file*} (greedy for assets). All captured params are injected after the Request argument.
  • Named routes: router->url('blog.show', ['slug' => 'hello']) builds /blog/hello.
  • Events: router.matching fires before pattern evaluation; router.matched includes the resolved params.

Middleware system #

config/http.php defines aliases and the global stack:

return [
    'middleware' => [
        'aliases' => [
            'errors' => ErrorHandlingMiddleware::class,
            'csrf' => VerifyCsrfToken::class,
        ],
        'global' => [
            'errors',
            // e.g. 'csrf',
        ],
    ],
];
  • Aliases let you refer to middleware by short names ('auth', 'throttle', etc.).
  • Global stack is prepended to every request. Add entries here to enforce policies application-wide.
  • Use $router->middleware('csrf') or $router->middleware([ 'auth', fn($request, $next) => $next($request) ]); to attach per-route middleware.

VelvetCMS\Http\Middleware\Pipeline resolves aliases or class names through the application container, so constructor injection works for sessions, guards, etc.

Built-in middleware #

Middleware Purpose
ErrorHandlingMiddleware Wraps downstream handlers, reports exceptions, and renders friendly responses via the shared exception handler.
VerifyCsrfToken Validates _token or X-CSRF-TOKEN headers on POST/PUT/PATCH/DELETE, with glob-style except patterns.
ThrottleRequests Rate limits requests based on IP address using global configuration.

Rate Limiting #

VelvetCMS includes a built-in rate limiter to protect your application from abuse. It uses the configured cache driver to store hit counts.

Usage #

Apply the throttle middleware to your routes:

// Limit based on config/http.php settings
$router->get('/api/users', [UserController::class, 'index'])
    ->middleware('throttle');

If the limit is exceeded, a 429 Too Many Requests response is returned with Retry-After and X-RateLimit-* headers.

Configuration #

The rate limiter is configured globally in config/http.php. It does not currently support per-route limits.

// config/http.php
return [
    'rate_limit' => [
        'max_attempts' => 60,
        'decay_minutes' => 1,
    ],
    // ...
];

Ensure your cache is configured correctly in config/cache.php.

Add your own by implementing MiddlewareInterface::handle(Request $request, callable $next): Response or creating an invokable class. Register aliases in config to keep route files tidy.

Route cache workflow #

  1. velvet route:cache executes routes/web.php, captures Router::getRouteDefinitions(), and writes them to storage/cache/routes.php.
  2. public/index.php will load that array instead of re-registering routes on every request.
  3. velvet route:clear deletes the cache file.

Always clear + rebuild when routes change (CI scripts usually run route:clear && route:cache).

Serving theme assets #

routes/web.php includes a hardened /themes/{theme}/assets/{asset*} route:

  • Sanitizes the theme name + asset path to prevent traversal.
  • Streams the file with Response::file() and strong caching headers (Cache-Control, Last-Modified, and conditional 304 logic based on If-None-Match + If-Modified-Since).
  • Honors theme.assets.max_age from config/theme.php.

Use it as a template for your own asset routes (e.g., module static files) or contribute a CDN-friendly variant via a module.

Tips #

  • Centralize shared closures in controllers or invokable classes; the router happily resolves dependencies.
  • Prefer middleware for cross-cutting concerns (auth, localization, feature flags) so routes stay declarative.
  • Emit your own events (e.g., router.matched) within modules to monitor API usage without changing controllers.