Menaikkan level HTTP server Swoole dengan routing statis dan regex, pola dispatcher yang rapi, lalu membangun pipeline middleware untuk CORS, logging, error handling, dan rate limiting tanpa framework.

Setelah di episode 4 kita membangun HTTP server dasar dengan routing manual yang masih statis — pada episode kali ini kita menaikkan level: routing lanjutan (regex, parameterized routes, dispatcher yang rapi) dan middleware pipeline untuk menangani hal-hal lintas-route seperti CORS, logging, dan error handling.
Mengapa episode ini penting? Karena aplikasi nyata tidak pernah se-sederhana hello world. Pola yang kita bangun di sini — dispatcher + middleware — adalah pola yang sama persis dipakai framework-framework besar (Hyperf, Laravel Octane). Menguasainya secara manual akan membuat kalian memahami cara kerja framework, bukan sekadar pemakainya.
Route statis cocok untuk daftar endpoint tetap, tapi aplikasi nyata butuh parameter di path, misal /users/42 atau /posts/2026/08:
<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
$server = new Server('0.0.0.0', 9501);
function dispatch(string $path): ?callable
{
$routes = [
'/^\/users\/(\d+)$/' => function (Request $req, Response $res, array $m) {
$res->end("User ID: {$m[1]}");
},
'/^\/posts\/(\d{4})\/(\d{2})$/' => function (Request $req, Response $res, array $m) {
$res->end("Posts {$m[1]}-{$m[2]}");
},
];
foreach ($routes as $pattern => $handler) {
if (preg_match($pattern, $path, $matches)) {
array_shift($matches);
return fn (Request $req, Response $res) => $handler($req, $res, $matches);
}
}
return null;
}
$server->on('Request', function (Request $req, Response $res) {
$path = rtrim($req->server['request_uri'] ?? '/', '/');
$handler = dispatch($path);
if ($handler === null) {
$res->status(404);
$res->end('Not Found');
return;
}
$handler($req, $res);
});
$server->start();Keuntungan regex routes: pola {id} yang dibatasi digit tidak akan ketuker dengan route lain, dan capture group langsung menjadi parameter — pola yang sama dipakai library routing kelas berat.
Dispatcher di atas masih mencampur pencocokan dan eksekusi. Untuk skala lebih besar, pisahkan menjadi tiga bagian:
pattern => handler (bisa di-cache di memori, episode 4).Pisahan ini membuat kode mudah dites tanpa harus menyalakan server. Untuk proyek besar, pertimbangkan library router murni (misal nikic/fast-route atau league/route) yang bisa digabung dengan dispatcher kalian — konsepnya sama, tinggal implementasinya dipercayakan ke library.
Middleware adalah fungsi yang membungkus handler — berjalan sebelum (memodifikasi request) dan/atau sesudah (memodifikasi response). Dalam Swoole, kita bisa menumpuknya dalam array dan menjalankannya berurutan:
Implementasi sederhana: setiap middleware adalah closure yang menerima $req, $res, dan $next:
<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
$middlewares = [
function (Request $req, Response $res, callable $next) {
$res->header('Access-Control-Allow-Origin', '*');
$res->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
$res->header('Access-Control-Allow-Headers', 'Content-Type');
if ($req->server['request_method'] === 'OPTIONS') {
$res->status(204);
$res->end();
return;
}
$next($req, $res);
},
function (Request $req, Response $res, callable $next) {
$start = microtime(true);
$next($req, $res);
$ms = round((microtime(true) - $start) * 1000, 2);
$res->header('X-Latency', "{$ms}ms");
},
];
function runPipeline(array $middlewares, Request $req, Response $res, callable $handler): void
{
$pipeline = $handler;
foreach (array_reverse($middlewares) as $mw) {
$pipeline = fn (Request $r, Response $s) => $mw($r, $s, $pipeline);
}
$pipeline($req, $res);
}
$server = new Server('0.0.0.0', 9501);
$server->on('Request', function (Request $req, Response $res) {
$handler = fn (Request $r, Response $s) => $s->end('Hello Middleware!');
runPipeline($middlewares, $req, $res, $handler);
});
$server->start();Catatan penting dari contoh di atas: array_reverse dipakai karena middleware terakhir di daftar adalah yang paling dekat dengan handler — pola standar di framework (disebut onion model).
Middleware pertama di pipeline sudah mencontohkan CORS: set header Access-Control-* dan tangani request OPTIONS (preflight) dengan response 204 tanpa meneruskan ke handler.
Buat log request dengan format terstruktur:
function (Request $req, Response $res, callable $next) {
$start = microtime(true);
$next($req, $res);
$line = sprintf(
'%s %s %.2fms',
$req->server['request_method'] ?? 'GET',
$req->server['request_uri'] ?? '/',
(microtime(true) - $start) * 1000
);
echo date('c') . " $line\n";
}Di production, ganti echo dengan logger async (misal monolog + buffer, atau tulis lewat task worker — episode 11).
Bungkus handler dalam try/catch di level on('Request'):
$server->on('Request', function (Request $req, Response $res) {
try {
$handler = dispatch($req->server['request_uri']);
if ($handler === null) {
$res->status(404);
$res->end('Not Found');
return;
}
$handler($req, $res);
} catch (Throwable $e) {
error_log($e->getMessage());
$res->status(500);
$res->end('Internal Server Error');
}
});Jangan pernah biarkan exception bocor tanpa response — client akan menggantung menunggu timeout.
end() dipanggil handler sebelum middleware mengubah response — jalankan middleware sebelum handler, atau atur header setelahnya.end() — memanggil method Response setelah end() akan melempar error. Selalu cek status.\d+ bukan .*).Request di Swoole tidak immutable seperti PSR-7; jika kalian mengubah header di middleware, pastikan handler membaca dari objek yang sama.Pada episode 5 ini, kalian telah membangun routing lanjutan dan pipeline middleware.
Inti yang harus dibawa pulang:
array_reverse).Di episode 6 selanjutnya, kita masuk ke dunia realtime: WebSocket server dengan Swoole\WebSocket\Server — koneksi dua arah, event open/message/close, dan broadcast ke banyak client. Sampai jumpa di episode 6!