Middleware
Use middleware for shared HTTP concerns, attach it to routes or groups, and keep business logic out of it.
What middleware is for
In Lemonade, middleware is a PSR-15 layer around HTTP request processing. It fits request conditions, headers, CSRF, or access checks before the controller. Keep the boundary clear: middleware = shared HTTP behavior, controller = a concrete HTTP use case and orchestration, service = application or business operations. For a complete CSRF form example, continue to Forms & POST Workflow.
Attach middleware to a route or group
Use route middleware when one endpoint needs an extra HTTP rule. Use group middleware when several routes share the same concern. In both cases you register classes by name and the runtime resolves them from the container before the controller.
<?php
use App\Controllers\ArticleController;
use App\Controllers\ProfileController;
use App\Http\Middleware\RequirePreviewHeaderMiddleware;
use App\Http\Middleware\RequireProfileHeaderMiddleware;
use Lemonade\Framework\Routing\Router;
return static function (Router $router): void {
$router
->getNamed('articles.preview', '/articles/preview', ArticleController::class . '@preview')
->middleware(RequirePreviewHeaderMiddleware::class);
$router
->group('/profile', static function (Router $router): void {
$router->getNamed('profile.show', '', ProfileController::class . '@show');
$router->postNamed('profile.update', '/edit', ProfileController::class . '@update');
})
->middleware(RequireProfileHeaderMiddleware::class);
};Ordering and short-circuiting
Middleware runs in registration order. Order matters because one layer can modify the request, add response headers, or stop the pipeline early. return $handler->handle($request); passes the request on. Returning your own response ends that branch without running the rest of the pipeline.
<?php
use App\Http\Middleware\RequestIdMiddleware;
use Lemonade\Framework\Core\Context\ApplicationContextFactory;
use Lemonade\Framework\Core\KernelFactory;
use Lemonade\Framework\Http\Middleware\ErrorHandlingMiddleware;
use Lemonade\Framework\Http\Middleware\MiddlewareStack;
$context = (new ApplicationContextFactory())->fromGlobals(__DIR__);
$kernel = (new KernelFactory())->create($context);
$kernel->framework()->middleware(static function (MiddlewareStack $stack): void {
$stack->insertAfter(ErrorHandlingMiddleware::class, RequestIdMiddleware::class);
});
$kernel->handle();Write a small custom middleware
Custom middleware implements MiddlewareInterface and receives dependencies through constructor injection. Keep it small and focused on HTTP. Once it starts handling persistence or reusable business logic, move that work into an application service.
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class RequirePreviewHeaderMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly ResponseFactoryInterface $responses,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getHeaderLine('X-Preview-Mode') !== 'allow') {
return $this->responses->createResponse(403);
}
return $handler->handle($request);
}
}