DEV Community

Cover image for Building Lightweight PHP Microservices with webrium/core — No Framework Bloat Required
Benyamin Khalife
Benyamin Khalife

Posted on

Building Lightweight PHP Microservices with webrium/core — No Framework Bloat Required

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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,
    ];
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]);
});
Enter fullscreen mode Exit fullscreen mode

Custom 404 handler:

Route::setNotFoundHandler(fn() => ['error' => 'Route not found', 'code' => 404]);
Enter fullscreen mode Exit fullscreen mode

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];
});
Enter fullscreen mode Exit fullscreen mode

Pass null (or call input() with no arguments) to get the full input array:

Route::post('/echo', fn() => input());
Enter fullscreen mode Exit fullscreen mode

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.'];
});
Enter fullscreen mode Exit fullscreen mode

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']];
Enter fullscreen mode Exit fullscreen mode

You can also read the payload without verification (useful for debugging):

$payload = Jwt::getPayload($token);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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']);
});
Enter fullscreen mode Exit fullscreen mode

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' => '...']);
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

For stricter control:

App::configureCors([
    'allowed_origins'   => ['https://app.example.com'],
    'allowed_methods'   => ['GET', 'POST', 'PUT', 'DELETE'],
    'allow_credentials' => true,
]);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

github.com/webrium/core

Contributions, issues, and feedback are welcome.

Top comments (0)