SPIN Framework uses a JSON-based routing system that defines routes, middleware, and controllers in a structured configuration file. Routes are organized into groups with shared middleware and prefixes.
SPIN applications define routes in JSON configuration files (e.g., routes-dev.json) that specify the routing structure, middleware, and controller mappings.
{
"common": {
"before": [],
"after": [
"\\App\\Middlewares\\RequestIdAfterMiddleware",
"\\App\\Middlewares\\ResponseTimeAfterMiddleware",
"\\App\\Middlewares\\ResponseLogAfterMiddleware"
]
},
"groups": [
{
"name": "unversioned api endpoints",
"notes": "",
"prefix": "/api",
"before": [],
"routes": [
{ "methods":["GET"], "path":"/health", "handler":"\\App\\Controllers\\Api\\HealthController" },
{ "methods":["GET"], "path":"/status", "handler":"\\App\\Controllers\\Api\\StatusController" },
{ "methods":["GET"], "path":"/info", "handler":"\\App\\Controllers\\Api\\InfoController" }
],
"after": []
},
{
"name":"Public",
"notes": "Public endpoints",
"prefix": "/api/v1",
"before": [],
"routes": [],
"after": []
},
{
"name":"Private",
"notes": "Private endpoints",
"prefix": "/api/v1",
"before": [
"\\App\\Middlewares\\AuthHttpBeforeMiddleware"
],
"routes": [],
"after": []
},
{
"name":"Default",
"notes": "Default route if nothing else matches",
"prefix": "",
"before": [
"\\App\\Middlewares\\SessionBeforeMiddleware"
],
"routes": [
{ "methods":[], "path":"/", "handler":"\\App\\Controllers\\IndexController" }
],
"after": []
}
],
"errors": {
"4xx": "\\App\\Controllers\\Error4xxController",
"5xx": "\\App\\Controllers\\Error5xxController"
}
}Route groups allow you to organize related routes with shared middleware and prefixes.
{
"name": "Group Name",
"notes": "Description of the group",
"prefix": "/api/v1",
"before": ["\\App\\Middlewares\\AuthMiddleware"],
"routes": [
// Route definitions
],
"after": ["\\App\\Middlewares\\LoggingMiddleware"]
}- name: Descriptive name for the route group
- notes: Additional information about the group
- prefix: URL prefix applied to all routes in the group
- before: Middleware executed before route handling
- routes: Array of route definitions
- after: Middleware executed after route handling
Individual routes define the HTTP methods, path, and controller handler.
{
"methods": ["GET", "POST"],
"path": "/users/{id}",
"handler": "\\App\\Controllers\\UserController"
}- methods: Array of HTTP methods (GET, POST, PUT, DELETE, etc.)
- path: URL path with optional parameters
- handler: Fully qualified class name of the controller
// Single method
{ "methods": ["GET"], "path": "/users", "handler": "\\App\\Controllers\\UserController" }
// Multiple methods
{ "methods": ["GET", "POST"], "path": "/users", "handler": "\\App\\Controllers\\UserController" }
// All methods (empty array)
{ "methods": [], "path": "/", "handler": "\\App\\Controllers\\IndexController" }SPIN supports route parameters using curly brace syntax.
// Single parameter
{ "methods": ["GET"], "path": "/users/{id}", "handler": "\\App\\Controllers\\UserController" }
// Multiple parameters
{ "methods": ["GET"], "path": "/users/{id}/posts/{postId}", "handler": "\\App\\Controllers\\PostController" }
// Optional parameters (using ?)
{ "methods": ["GET"], "path": "/users/{id?}", "handler": "\\App\\Controllers\\UserController" }Controllers handle the business logic for routes and extend SPIN's base controller classes.
<?php declare(strict_types=1);
namespace App\Controllers;
use \App\Controllers\AbstractPlatesController;
class IndexController extends AbstractPlatesController
{
/**
* Handle GET request
*
* @param array $args Path variable arguments as name=value pairs
*/
public function handleGET(array $args)
{
# Model to send to view
$model = ['title'=>'PageTitle', 'user'=>'Friend'];
# Render view
$html = $this->engine->render('pages::index', $model);
# Send the generated html
return response($html);
}
}SPIN controllers use specific method names to handle different HTTP requests:
handleGET(array $args)- Handles GET requestshandlePOST(array $args)- Handles POST requestshandlePUT(array $args)- Handles PUT requestshandleDELETE(array $args)- Handles DELETE requestshandlePATCH(array $args)- Handles PATCH requestshandleQUERY(array $args)- Handles QUERY requests
QUERY is safe and idempotent like GET but carries a request body. Read it via
getRequest()->getBody()(same as POST). See IETFdraft-ietf-httpbis-safe-method-w-bodyand OpenAPI 3.2.
The $args parameter contains route parameters as key-value pairs:
// Route: /users/{id}/posts/{postId}
// URL: /users/123/posts/456
public function handleGET(array $args)
{
$userId = $args['id']; // "123"
$postId = $args['postId']; // "456"
// Controller logic here
}SPIN provides abstract controller classes for common functionality.
For template-based responses using the Plates template engine:
<?php declare(strict_types=1);
namespace App\Controllers;
use \App\Controllers\AbstractPlatesController;
class UserController extends AbstractPlatesController
{
public function handleGET(array $args)
{
$user = getUserById($args['id']);
$html = $this->engine->render('users::show', ['user' => $user]);
return response($html);
}
}For API responses returning JSON:
<?php declare(strict_types=1);
namespace App\Controllers;
use \App\Controllers\AbstractRestController;
class ApiController extends AbstractRestController
{
public function handleGET(array $args)
{
$data = ['status' => 'success', 'message' => 'Hello World'];
return responseJson($data);
}
}Routes can have middleware applied at multiple levels:
Applied to all routes in the application:
{
"common": {
"before": ["\\App\\Middlewares\\RequestIdBeforeMiddleware"],
"after": ["\\App\\Middlewares\\ResponseLogAfterMiddleware"]
}
}Applied to all routes in a specific group:
{
"name": "Private",
"prefix": "/api/v1",
"before": ["\\App\\Middlewares\\AuthHttpBeforeMiddleware"],
"routes": [
{ "methods": ["GET"], "path": "/profile", "handler": "\\App\\Controllers\\ProfileController" }
]
}Note: Middleware can only be applied at the common (global) level or the group level. There is no route-level middleware — use a dedicated group with a prefix to scope middleware to specific routes.
SPIN provides error controllers for handling HTTP error responses.
{
"errors": {
"4xx": "\\App\\Controllers\\Error4xxController",
"5xx": "\\App\\Controllers\\Error5xxController"
}
}<?php declare(strict_types=1);
namespace App\Controllers;
use \App\Controllers\AbstractPlatesController;
class Error4xxController extends AbstractPlatesController
{
public function handleGET(array $args)
{
$statusCode = $args['status'] ?? 404;
$html = $this->engine->render('errors::4xx', ['status' => $statusCode]);
return response($html, $statusCode);
}
}SPIN provides helper functions for creating responses.
// HTML response
return response($html);
// JSON response
return responseJson($data);
// Response with status code
return response($html, 201);
// Response with headers
return response($html, 200, ['Content-Type' => 'text/html']);
// JSON response with status
return responseJson($data, 201);SPIN caches route definitions for performance. Changes to route configuration require an application restart.
- Route Organization: Group related routes together with descriptive names
- Middleware Order: Apply authentication middleware early in the pipeline
- Error Handling: Provide meaningful error responses for different HTTP status codes
- Controller Methods: Use specific HTTP method handlers for better organization
- Route Parameters: Use descriptive parameter names and validate them in controllers
- Response Consistency: Use consistent response formats across your API
- Documentation: Document route groups and their purposes
Here's a complete example of a typical SPIN application route configuration:
{
"common": {
"before": ["\\App\\Middlewares\\RequestIdBeforeMiddleware"],
"after": [
"\\App\\Middlewares\\ResponseTimeAfterMiddleware",
"\\App\\Middlewares\\ResponseLogAfterMiddleware"
]
},
"groups": [
{
"name": "Public API",
"prefix": "/api/v1",
"before": ["\\App\\Middlewares\\CorsBeforeMiddleware"],
"routes": [
{ "methods": ["GET"], "path": "/health", "handler": "\\App\\Controllers\\Api\\HealthController" },
{ "methods": ["POST"], "path": "/auth/login", "handler": "\\App\\Controllers\\Api\\AuthController" }
]
},
{
"name": "Protected API",
"prefix": "/api/v1",
"before": [
"\\App\\Middlewares\\AuthHttpBeforeMiddleware",
"\\App\\Middlewares\\RateLimitBeforeMiddleware"
],
"routes": [
{ "methods": ["GET"], "path": "/users/{id}", "handler": "\\App\\Controllers\\Api\\UserController" },
{ "methods": ["POST"], "path": "/users", "handler": "\\App\\Controllers\\Api\\UserController" },
{ "methods": ["PUT"], "path": "/users/{id}", "handler": "\\App\\Controllers\\Api\\UserController" },
{ "methods": ["DELETE"], "path": "/users/{id}", "handler": "\\App\\Controllers\\Api\\UserController" }
]
},
{
"name": "Web Pages",
"prefix": "",
"before": ["\\App\\Middlewares\\SessionBeforeMiddleware"],
"routes": [
{ "methods": [], "path": "/", "handler": "\\App\\Controllers\\IndexController" },
{ "methods": ["GET"], "path": "/about", "handler": "\\App\\Controllers\\PageController" },
{ "methods": ["GET"], "path": "/contact", "handler": "\\App\\Controllers\\PageController" }
]
}
],
"errors": {
"4xx": "\\App\\Controllers\\Error4xxController",
"5xx": "\\App\\Controllers\\Error5xxController"
}
}This routing system provides a clean, organized way to define your application's URL structure while maintaining flexibility for middleware and controller organization.