Do you really need a full framework to handle a few API endpoints or webhooks?
Laravel and Symfony are excellent tools — for large applications. But when you're building a focused microservice, a webhook receiver, or a lightweight REST API, bootstrapping a full-stack framework means carrying hundreds of files, a massive autoloader, and a dependency tree you'll never fully use.
That's the problem webrium/core was built to solve: a minimalist, zero-dependency PHP micro-framework written entirely from scratch, designed to stay out of your way.
Installation
composer require webrium/core
That's it. No configuration files to publish, no service providers to register.
The Entry Point
Every webrium application starts with the same three lines:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Webrium\App;
use Webrium\Route;
App::initialize(__DIR__);
// ... your routes here
App::run();
App::initialize() sets the root path and loads the global helper functions. App::run() initializes error handling and dispatches the current request through the router.
Routing
The router supports all standard HTTP methods. Route handlers can be closures, a Controller@method string, or an [Controller::class, 'method'] array.
Basic routes:
Route::get('/status', fn() => ['status' => 'alive']);
Route::post('/items', fn() => ['created' => true]);
Route::put('/items/{id}', fn($id) => ['updated' => $id]);
Route::patch('/items/{id}', fn($id) => ['patched' => $id]);
Route::delete('/items/{id}', fn($id) => ['deleted' => $id]);
Route handlers return an array — the framework automatically encodes it as JSON and sends the correct Content-Type header.
Dynamic parameters:
Route::get('/users/{id}/posts/{postId}', function($id, $postId) {
return [
'user_id' => $id,
'post_id' => $postId,
];
});
Named routes:
Route::get('/users/{id}', fn($id) => ['id' => $id])->name('users.show');
// Generate the URL elsewhere:
$url = route('users.show', ['id' => 42]); // /users/42
Route groups let you share a prefix and/or middleware across multiple routes:
Route::group(['prefix' => 'api/v1', 'middleware' => 'AuthMiddleware'], function() {
Route::get('/profile', fn() => ['user' => 'James Carter']);
Route::post('/profile', fn() => ['updated' => true]);
});
Custom 404 handler:
Route::setNotFoundHandler(fn() => ['error' => 'Route not found', 'code' => 404]);
Reading Request Input
The global input() helper reads from $_GET on GET requests and from the request body on POST, PUT, PATCH, and DELETE. It handles both application/json and application/x-www-form-urlencoded automatically.
Route::post('/search', function() {
$query = input('q', '');
$limit = input('limit', 20);
return ['query' => $query, 'limit' => $limit];
});
Pass null (or call input() with no arguments) to get the full input array:
Route::post('/echo', fn() => input());
Validation
The Validator class uses a fluent interface. Pass the input array to its constructor, chain your rules, then call isValid(), passes(), or fails().
use Webrium\Validator;
Route::post('/register', function() {
$v = new Validator(input());
$v->field('name', 'Full Name')->required()->string()->min(2)->max(50);
$v->field('email', 'Email')->required()->email();
$v->field('password', 'Password')->required()->min(8);
$v->field('password_confirmation')->required()->confirmed('password');
if ($v->fails()) {
return respond(['errors' => $v->getErrors()], 422);
}
// proceed with registration...
return ['success' => true, 'message' => 'Account created.'];
});
Available rules include: required, string, integer, numeric, boolean, email, min, max, confirmed, different, in, notIn, regex, date, json, array, nullable, and sometimes.
JWT Authentication
The Jwt class handles token generation and verification with HS256, HS384, and HS512. Signature verification uses hash_equals internally to prevent timing attacks.
use Webrium\Jwt;
$jwt = new Jwt('your-secret-key', 'HS256');
// Generate a token
$token = $jwt->generateToken([
'sub' => 42,
'role' => 'admin',
'exp' => time() + 3600,
]);
// Verify and decode
$payload = $jwt->verifyToken($token);
if ($payload === null) {
return respond(['error' => 'Invalid or expired token.'], 401);
}
return ['user_id' => $payload['sub']];
You can also read the payload without verification (useful for debugging):
$payload = Jwt::getPayload($token);
Password Hashing
The Hash class wraps PHP's password_hash and password_verify with sensible defaults and supports bcrypt, Argon2i, and Argon2id.
use Webrium\Hash;
// Hash a password (bcrypt by default)
$hashed = Hash::make('secret-password');
// Verify
$isValid = Hash::check('secret-password', $hashed); // true
// Argon2id
$hashed = Hash::argon2id('secret-password');
// Generate a secure random token
$token = Hash::token(64);
// Generate a UUID v4
$uuid = Hash::uuid();
Middleware
Middleware can be a closure, a global function, a Class@method string, or a class with a handle() method that returns a boolean. A falsy return blocks the request with a 403.
// Closure middleware
$authCheck = function() {
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
return str_starts_with($header, 'Bearer ');
};
Route::group(['prefix' => 'api/v1', 'middleware' => $authCheck], function() {
Route::get('/me', fn() => ['user' => 'James Carter']);
});
Class-based middleware:
class AuthMiddleware
{
public function handle(): bool
{
$jwt = new Webrium\Jwt('your-secret-key');
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $header);
return $jwt->verifyToken($token) !== null;
}
}
Route::group(['middleware' => 'AuthMiddleware'], function() {
Route::get('/dashboard', fn() => ['data' => '...']);
});
CORS
CORS is handled at the application level with a single call before App::run().
App::enableCors(['https://app.example.com', 'https://admin.example.com']);
App::run();
For stricter control:
App::configureCors([
'allowed_origins' => ['https://app.example.com'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allow_credentials' => true,
]);
Putting It All Together
Here is a minimal but complete authenticated REST API — health check, public registration, and a protected profile endpoint — in a single file:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Webrium\App;
use Webrium\Route;
use Webrium\Validator;
use Webrium\Hash;
use Webrium\Jwt;
App::initialize(__DIR__);
App::enableCors(['https://app.example.com']);
$jwt = new Jwt('super-secret-key');
// Health check
Route::get('/', fn() => ['status' => 'alive']);
// Public: register
Route::post('/register', function() {
$v = new Validator(input());
$v->field('email')->required()->email();
$v->field('password')->required()->min(8);
if ($v->fails()) {
return respond(['errors' => $v->getErrors()], 422);
}
$hashed = Hash::make(input('password'));
// save user to DB...
return respond(['message' => 'Account created.'], 201);
});
// Public: login
Route::post('/login', function() use ($jwt) {
$email = input('email', '');
$password = input('password', '');
// fetch user from DB and verify password...
$user = ['id' => 1, 'email' => $email, 'hashed' => Hash::make('demo')];
$passwordValid = Hash::check($password, $user['hashed']);
if (!$passwordValid) {
return respond(['error' => 'Invalid credentials.'], 401);
}
$token = $jwt->generateToken(['sub' => $user['id'], 'exp' => time() + 3600]);
return ['token' => $token];
});
// Protected routes
$authMiddleware = function() use ($jwt) {
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $header);
return $jwt->verifyToken($token) !== null;
};
Route::group(['prefix' => 'api', 'middleware' => $authMiddleware], function() use ($jwt) {
Route::get('/profile', function() use ($jwt) {
$token = str_replace('Bearer ', '', $_SERVER['HTTP_AUTHORIZATION'] ?? '');
$payload = $jwt->verifyToken($token);
return ['user_id' => $payload['sub']];
});
});
App::run();
A complete, production-ready authenticated API service. No generated boilerplate, no hidden magic — just PHP.
webrium/core is a good fit when:
- You're building a webhook receiver or a focused REST API
- You need minimal cold-start time (serverless, edge environments)
- You want zero third-party runtime dependencies
- You're prototyping quickly and don't want framework overhead
It is not a replacement for a full-stack framework when you need an ORM,
a template engine, queue workers, or a large ecosystem of packages.
If you need all of that — but still want to stay in the Webrium ecosystem —
webrium/webrium is the full-stack
version, built on top of this same core. It ships with
FoxDB as its ORM and includes a
dedicated template engine. Same philosophy, full feature set.
Source Code
The project is open source and available on GitHub:
Contributions, issues, and feedback are welcome.
Top comments (0)