Narciso is a lightweight web library built on top of native PHP, inspired by FastAPI and Flask. It gives you a simple, expressive API for routing, middlewares, CORS, rate limiting, security headers, and database access—so you can build APIs and web apps quickly without a heavy framework.
Packagist: https://packagist.org/packages/marcuwynu23/narciso
- Routing — Define routes with path parameters (e.g.
/users/:id), multiple HTTP methods. - Middlewares — Add middlewares in order (security, CORS, rate limit, or your own). User-friendly
use()API. - Easy database integration — One config for MySQL or SQLite;
$app->dbready to use. - Cross-Origin (CORS) — Configurable origins, methods, and headers; no more guessing.
- Rate limiting — Built-in per-IP rate limit middleware; plug in Redis later for production.
- Security — Optional security headers (X-Frame-Options, X-Content-Type-Options, etc.) and secure defaults.
- Technology signature — Make the stack silent or changeable: remove
X-Powered-By, set it blank, or fake it (e.g. "Express") so the server type is obfuscated. - HTTP — JSON requests/responses, redirects, views, session handling.
composer require marcuwynu23/narciso<?php
require_once __DIR__ . '/vendor/autoload.php';
use Marcuwynu23\Narciso\Application;
$app = new Application();
$app->setViewPath(__DIR__ . '/views');
// Optional: hide or change technology signature (X-Powered-By)
$app->setPoweredBy(false); // silent, or setPoweredBy('Express') etc.
// Optional: session
$app->handleSession();
// Middlewares (order matters: first added = first run)
$app->useSecurityHeaders(); // Security headers on every response
$app->useCors(['*']); // CORS: allow all origins (or list specific origins)
$app->useRateLimit(60, 60); // 60 requests per 60 seconds per IP
// Database: one call, then use $app->db everywhere
$app->handleDatabase([
'type' => 'mysql',
'host' => 'localhost',
'database' => 'mydb',
'username' => 'user', // or 'user'
'password' => 'secret',
]);
// Routes (path params like FastAPI/Flask)
$app->route('GET', '/', function ($app) {
$app->render('/home/index.view');
});
$app->route('GET', '/json', function ($app) {
$app->json(['message' => 'Hello World']);
});
$app->route('GET', '/users/:id', function ($app, $params) {
$id = $params['id'] ?? null;
$app->json(['user_id' => $id]);
});
// Run the app (dispatches request through middlewares and routes)
$app->run();Add middlewares with $app->use(...) or the built-in helpers. They run in the order you add them.
| Method | Description |
|---|---|
useSecurityHeaders(?array $headers) |
Sends security headers (X-Content-Type-Options, X-Frame-Options, etc.). Pass null for defaults or your own array. |
useCors($origins, $methods, $headers, $credentials, $maxAge) |
Configurable CORS. See CORS below. |
useRateLimit($maxRequests, $windowSeconds) |
Rate limit per client IP (in-memory; use Redis for multi-process). |
Option 1 — Implement interface (recommended):
use Marcuwynu23\Narciso\Middleware\MiddlewareInterface;
class MyMiddleware implements MiddlewareInterface {
public function handle(callable $next) {
// Before request
$result = $next();
// After request (if $next returns)
return $result;
}
}
$app->use(new MyMiddleware());Option 2 — Callable (Flask-like):
$app->use(function ($app, $next) {
// Before
$result = $next();
// After
return $result;
});One-time setup; then use $app->db in your routes.
MySQL:
$app->handleDatabase([
'type' => 'mysql',
'host' => 'localhost',
'database' => 'mydb',
'username' => 'user', // or 'user'
'password' => 'pass',
]);
// $app->db is mysqliSQLite:
$app->handleDatabase([
'type' => 'sqlite',
'database' => __DIR__ . '/data/app.db',
]);
// $app->db is SQLite3Use useCors() for full control (recommended over the legacy handleCORS()):
// Allow all origins (default)
$app->useCors();
// Restrict to your frontend (PHP 8+ named args; or pass by position)
$app->useCors(
['https://app.example.com', 'http://localhost:3000'],
['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
['Content-Type', 'Authorization'],
true, // credentials
86400 // maxAge
);Built-in per-IP rate limit (in-memory). For production with multiple processes, replace with a Redis-backed middleware.
$app->useRateLimit(100, 60); // 100 requests per 60 seconds per IPWhen exceeded, responses are 429 Too Many Requests with a Retry-After header.
- Security headers —
$app->useSecurityHeaders()sends headers like:X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockReferrer-Policy,Permissions-Policy
- Custom headers:
$app->useSecurityHeaders(['X-Custom' => 'value']); - CORS — Prefer
useCors()with explicit origins instead of*when using credentials. - Database — Use parameterized queries with
$app->db; never concatenate user input into SQL. - Technology signature (X-Powered-By) — Like Express’s
app.disable('x-powered-by'): call$app->setPoweredBy(false)to remove the header (silent/obfuscated),$app->setPoweredBy('')for a blank value, or$app->setPoweredBy('Express')to send a custom value so the server type is not revealed.
Control or hide the X-Powered-By header so the server technology (e.g. PHP) is silent or changeable (Express-style):
// Remove the header (obfuscated / silent)
$app->setPoweredBy(false);
// Send a blank value
$app->setPoweredBy('');
// Send a custom value (e.g. pretend another stack)
$app->setPoweredBy('Express');
// Leave default (don't touch; default behavior)
$app->setPoweredBy(null); // or omitApplied automatically at the start of run().
Routes support path parameters (e.g. /users/:id). Your callback receives ($app, $params).
$app->route('GET', '/posts/:slug', function ($app, $params) {
$slug = $params['slug'];
$app->json(['slug' => $slug]);
});Use $app->sendAPI() to respond as JSON or XML (legacy). Format can be forced or auto-detected from query param (?format=json or ?format=xml) or Accept header.
// Auto-detect from ?format=xml or Accept: application/xml
$app->route('GET', '/api/users', function ($app) {
$app->sendAPI(['users' => [['id' => 1, 'name' => 'Alice']]]);
});
// Force JSON
$app->sendAPI($data, ['format' => 'json']);
// Force XML (legacy)
$app->sendAPI($data, ['format' => 'xml']);
// Options: root tag, list item tag, status code
$app->sendAPI($data, [
'format' => 'xml',
'root' => 'data',
'xmlItemName' => 'user',
'statusCode' => 200,
]);- Default format is JSON; use
?format=xmlorAccept: application/xmlfor XML. - arrayToXml is public for custom XML:
$app->arrayToXml($array, 'rootTag', 'itemTag'). - getPreferredApiFormat() returns
'json'or'xml'from the current request.
PHP built-in server:
php -S localhost:8080 -t <entry_point_directory>With .autofile script:
+ php -S localhost:8080 -t <entry_point_directory>Then run:
autoNarciso is inspired by the myth of Narcissus and by the simplicity of FastAPI and Flask. It focuses on clarity and minimal setup: middlewares, database, CORS, rate limit, and security are a few method calls away, so you can spend time on your app instead of framework configuration.
Tests are test-driven and cover all main functionality: routing (exact and path params), middlewares (security, CORS, rate limit), setPoweredBy, JSON/render responses, 404, database (SQLite), and custom middlewares.
Run the test suite:
composer install
vendor/bin/phpunitOr from project root:
php test/run_tests.phpContributions are welcome. Open an issue or submit a pull request.
Happy coding!