A smart, lightweight and fast REST router for PHP.
- Overview
- Why ROSA Router?
- Features
- Requirements
- Installation
- Configuration
- Quick Start
- Usage
- Authentication
- Logging
- Testing
- License
ROSA Router is a lightweight and efficient REST API engine built with PHP. It handles incoming HTTP requests and routes them to the appropriate controllers or actions based on your defined endpoints. With a focus on simplicity and performance, ROSA Router lets you build and deploy RESTful web services quickly — in both stateless and stateful (long-running server) modes.
Most PHP routers force a choice up front: the classic stateless model — where the framework boots from scratch on every request — or a stateful, long-running server for lower latency. ROSA Router runs the same route definitions in both modes, so you can start simple on a shared host and later switch to a persistent ReactPHP server by changing a single flag — no rewrite.
On top of that it stays dependency-light and explicit: a clean routing syntax, first-class middleware, and authentication built in (JWT and API keys), without pulling in a full framework.
- 🚀 Easy routing — Define routes for your REST API with a clean, expressive syntax.
- 🔀 Full HTTP method support —
GET,POST,PUT,PATCHandDELETE. - 🧩 Route groups & prefixes — Organize routes with prefixes, nesting and namespaces.
- 🛡️ Middleware — Attach middleware to single routes or whole groups.
- 🔐 Built-in authentication — JWT and API key strategies out of the box.
- 📝 Request logging — Opt-in per-route logging to file or database.
- ⚡ Stateless or stateful — Run on a classic web server or as a long-running ReactPHP server.
- 🪶 Lightweight & fast — Minimal overhead, optimized for performance.
- 🧯 Built-in error handling — Gracefully manage exceptions and invalid requests.
- PHP 8.0 or higher
- Composer
- Extensions:
ext-json,ext-pdo
composer require rockberpro/rosa-routerROSA Router is configured through environment variables loaded at bootstrap. Copy the example file and adjust it for your environment:
cp .env.example .envBootstrap::setup() loads the configuration automatically. You can point it at a
custom path — both .env and .ini formats are supported:
Bootstrap::setup(); // loads ./.env
Bootstrap::setup('config/.env'); // custom path
Bootstrap::setup('config/.ini'); // INI format| Variable | Description | Example |
|---|---|---|
API_NAME |
Application name | rosa-api |
API_DEBUG |
Verbose error output | false |
API_LOGS / API_LOGS_DB |
Request-log destination — file / database (see Logging) | true / false |
API_ALLOW_ORIGIN |
CORS allowed origin | * |
API_SERVER_ADDRESS / API_SERVER_PORT |
Address & port for stateful mode | 0.0.0.0 / 8081 |
API_AUTH_METHOD |
Authentication strategy — JWT or KEY |
JWT |
JWT_ISSUER / JWT_SUBJECT / JWT_SECRET |
JWT signing settings | — |
API_DB_* |
Database connection (host, port, user, pass, name, type) | pgsql |
ROSA Router listens for HTTP requests and maps them to the correct route handler based on the request's method and URI. It supports both static and dynamic routes and is fully customizable to fit different project needs.
<?php
// index.php
use Rockberpro\RosaRouter\Bootstrap;
require_once "vendor/autoload.php";
// Bootstrap::setup('path/to/.env');
// Bootstrap::setup('path/to/.ini');
Bootstrap::setup();
$server = Server::init();
if ($server->isApiEndpoint()) {
$server->loadRoutes('./routes/api.php');
$server->execute(Server::MODE_STATELESS);
}<?php
// server.php — run with: php server.php
use Rockberpro\RosaRouter\Utils\DotEnv;
use Rockberpro\RosaRouter\Bootstrap;
use React\Socket\SocketServer;
use React\Http\HttpServer;
require_once "vendor/autoload.php";
// Bootstrap::setup('path/to/.env');
// Bootstrap::setup('path/to/.ini');
Bootstrap::setup();
$port = DotEnv::get('API_SERVER_PORT');
$address = DotEnv::get('API_SERVER_ADDRESS');
$server = Server::init();
$server->loadRoutes('./routes/api.php');
$server = new HttpServer(
$server->execute(Server::MODE_STATEFUL)
);
$server->on('error', function (Throwable $e) {
print("Request error: " . $e->getMessage() . PHP_EOL);
});
$socket = new SocketServer("{$address}:{$port}");
$server->listen($socket);
print("Server running at http://{$address}:{$port}" . PHP_EOL);A route maps an HTTP method and URI to a handler. The handler can be a
controller ([Controller::class, 'method'] or 'Controller@method') or an
inline closure. Every handler must return a Response.
use Rockberpro\RosaRouter\Core\Route;
Route::get('/post/{post}/comment/{comment}', [PostController::class, 'get']);
Route::get('/user/{id}', [UserController::class, 'get']);
Route::post('/user', [UserController::class, 'post']);
Route::put('/user/{id}', [UserController::class, 'put']);
Route::patch('/user/{id}', [UserController::class, 'patch']);
Route::delete('/user/{id}', [UserController::class, 'delete']);A handler can also be a closure that receives the Request and returns a Response:
use Rockberpro\RosaRouter\Core\Request;
use Rockberpro\RosaRouter\Core\Response;
use Rockberpro\RosaRouter\Core\Route;
Route::get('/ping', function (Request $request) {
return new Response(['message' => 'pong'], Response::OK);
});Inside a handler, use the Request instance to read incoming values. $request->get($key)
resolves the key from body, path and query parameters (in that order):
Route::get('/user/{id}', function (Request $request) {
$id = $request->get('id'); // path parameter -> /user/42
$fields = $request->get('fields'); // query parameter -> ?fields=name,email
return new Response(['id' => $id, 'fields' => $fields], Response::OK);
});Need them grouped by source? Use the dedicated accessors:
$request->getPathParam('id'); // route placeholders, e.g. {id}
$request->getQueryParam('page'); // ?page=2
$request->getBodyParam('email'); // JSON body fields
$request->getParams(); // everything, grouped by sourceA Response takes a payload (sent as JSON) and an HTTP status code. The Response
class exposes constants for the common codes:
return new Response(['message' => 'Created'], Response::CREATED); // 201
return new Response(['message' => 'Not found'], Response::NOT_FOUND); // 404
return new Response(['message' => 'Invalid'], Response::UNPROCESSABLE_ENTITY); // 422Use prefix() + group() to share a common URI segment across several routes
instead of repeating it on each one:
Route::prefix('v1')->group(function() {
Route::get('/users/{id}', [UserController::class, 'get']); // GET /api/v1/users/{id}
Route::post('/users', [UserController::class, 'post']); // POST /api/v1/users
});A group is also where you attach a middleware() or namespace() once and have
it apply to every route inside (see the Middleware and
Namespaces sections).
Groups can be nested to any depth. Each nested prefix() is appended to its
parent, so the final URI is the concatenation of every prefix in the chain.
All routes are served under the framework's /api base path:
Route::prefix('v1')->group(function() {
Route::get('/users/{id}', [UserController::class, 'get']); // GET /api/v1/users/{id}
Route::post('/users', [UserController::class, 'post']); // POST /api/v1/users
Route::prefix('users/{user}')->group(function() {
Route::get('/posts', [PostController::class, 'index']); // GET /api/v1/users/{user}/posts
Route::post('/posts', [PostController::class, 'store']); // POST /api/v1/users/{user}/posts
});
});Set a namespace() so you can reference controllers by their short
Controller@method string instead of the fully-qualified class name:
Route::namespace('App\\Controllers')->group(function() {
Route::get('/example', 'ExampleController@get');
Route::post('/example', 'ExampleController@post');
});namespace() composes with the other modifiers — combine it with prefix()
and middleware() on the same group when you need to:
Route::prefix('v1')
->namespace('App\\Controllers')
->middleware(AuthMiddleware::class)
->group(function() {
Route::get('/example', 'ExampleController@get');
});A middleware implements MiddlewareInterface. Its handle() method receives the
Request and a $next closure — call $next($request) to pass control along,
or return a Response early to short-circuit the request:
namespace App\Middleware;
use Closure;
use Rockberpro\RosaRouter\Middleware\MiddlewareInterface;
use Rockberpro\RosaRouter\Core\Request;
use Rockberpro\RosaRouter\Core\Response;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request, Closure $next): Response
{
if (!$request->get('token')) {
return new Response(['message' => 'Access denied'], Response::UNAUTHORIZED);
}
return $next($request);
}
}Attach it to a single route or to a whole group:
// Single route
Route::middleware(AuthMiddleware::class)
->get('/hello', 'HelloWorldController@hello');
// Whole group
Route::prefix('v1')
->middleware(AuthMiddleware::class)
->namespace('App\\Controllers')
->group(function() {
Route::get('/hello', 'HelloWorldController@hello');
});Middleware accumulates through nesting. When groups are nested, a route runs every middleware declared along its chain — an inner group does not discard the middleware inherited from an outer one. They execute outer-most first, and the same middleware declared at multiple levels runs only once:
Route::middleware(LogRequestMiddleware::class) // applies to everything below
->group(function() {
Route::get('/health', 'HealthController@check'); // [Log]
Route::middleware(AuthMiddleware::class)
->group(function() {
// runs [Log, Auth] — logging is NOT lost by the inner group
Route::get('/user/{id}', 'UserController@get');
});
});This makes a single outer group a practical way to apply a cross-cutting middleware (like request logging) to every route it wraps.
ROSA Router ships with a LogRequestMiddleware that records each incoming
request (endpoint, method, params, remote address, user agent). Logging works in
two independent layers:
-
Trigger — bind the middleware. Like any middleware, it only runs on routes you attach it to. Logging is opt-in, never automatic:
use Rockberpro\RosaRouter\Middleware\LogRequestMiddleware; Route::prefix('v1') ->middleware(LogRequestMiddleware::class) ->group(function() { Route::get('/hello', 'HelloWorldController@hello'); });
Because middleware accumulates through nesting, wrapping all your routes in one outer group is the simplest way to log everything — inner groups can still add their own middleware (e.g. auth) without losing the logging:
Route::middleware(LogRequestMiddleware::class)->group(function() { require 'routes/api.php'; // every route inside is logged });
-
Destination — pick where logs go via env (see Configuration):
API_LOGS=true— write to the info log file (logs/info.log).API_LOGS_DB=true— write to thelogsdatabase table.
You can enable either, both, or combine the middleware with others:
Route::middleware([AuthMiddleware::class, LogRequestMiddleware::class]) ->get('/user/{id}', [UserController::class, 'get']);
No silent failures. If you bind LogRequestMiddleware to a route but leave
both API_LOGS and API_LOGS_DB disabled, the request has nowhere to be
logged — a contradiction — and the router throws a LogHandlerException instead
of quietly dropping the log. Either enable a destination, or remove the
middleware from that route. A missing/undefined env variable likewise throws,
so misconfiguration always surfaces loudly.
A controller extends the base Controller class. Each action receives the
Request and returns a Response — use the response() helper as a shortcut:
namespace App\Controllers;
use Rockberpro\RosaRouter\Controllers\Controller;
use Rockberpro\RosaRouter\Core\Request;
use Rockberpro\RosaRouter\Core\Response;
class UserController extends Controller
{
public function get(Request $request): Response
{
$id = $request->get('id');
// ... fetch the user from your data source
if (!$id) {
return $this->response(['message' => 'User not found'], Response::NOT_FOUND);
}
return $this->response(['id' => $id, 'name' => 'Jane Doe'], Response::OK);
}
}Bind it to a route by class + method, or group several actions under the same controller:
// Explicit method binding
Route::get('/user/{id}', [UserController::class, 'get']);
// Group actions under one controller
Route::controller(UserController::class)->group(function() {
Route::get('/user/{id}', 'get');
Route::post('/user', 'post');
});ROSA Router ships with authentication built in — no extra package required.
Pick the strategy with API_AUTH_METHOD in your .env:
JWT— stateless JSON Web Tokens, signed with yourJWT_SECRET.KEY— API keys validated against the database.
Protect any route or group with the bundled AuthMiddleware:
use Rockberpro\RosaRouter\Middleware\AuthMiddleware;
Route::prefix('v1')
->middleware(AuthMiddleware::class)
->group(function() {
Route::get('/me', [UserController::class, 'me']); // requires a valid token / key
});When using JWT, the built-in endpoints issue and refresh tokens:
| Method & route | Description |
|---|---|
POST /api/auth/refresh |
Exchange credentials for an access + refresh token |
POST /api/auth/access |
Exchange a valid refresh token for a new access token |
Send the token on protected requests via the Authorization: Bearer <token> header.
The test suite runs on PHPUnit:
composer install
vendor/bin/phpunit testsROSA Router is open-source software licensed under the MIT License.
Made with ❤️ by rockberpro
