Building a REST API with Native PHP - No Framework Needed (2026)
Frameworks like Laravel make development easier, but for simple APIs that need maximum performance and zero dependencies, native PHP is a very viable choice. This article shows how to build a production-ready REST API without a single external library.
1. Project Structure
api/
boot.php # Bootstrap (headers, DB, helpers)
auth/
login.php # POST /api/auth/login
register.php # POST /api/auth/register
users/
me.php # GET /api/users/me
orders/
create.php # POST /api/orders/create
list.php # GET /api/orders/list
config/
db.php # PDO connection
helpers.php # Response helpers
.htaccess # URL routing
2. Bootstrap (boot.php)
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: https://yourapp.com");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {
http_response_code(204); exit;
}
require_once __DIR__ . "/../config/db.php";
require_once __DIR__ . "/../config/helpers.php";
3. Response Helper
function resp($success, $message, $data = null, $code = 200) {
http_response_code($code);
echo json_encode([
"success" => (bool)$success,
"message" => $message,
"data" => $data
]);
exit;
}
function get_input() {
$raw = file_get_contents("php://input");
return json_decode($raw, true) ?: [];
}
4. Bearer Token Authentication
function require_auth($pdo) {
$header = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
if (!preg_match("/Bearer (.+)/i", $header, $m)) {
resp(false, "Token required", null, 401);
}
$token_hash = hash("sha256", trim($m[1]));
$st = $pdo->prepare("SELECT u.* FROM sessions s
JOIN users u ON u.id = s.user_id
WHERE s.token = ? AND s.expires_at > NOW()");
$st->execute([$token_hash]);
$user = $st->fetch();
if (!$user) resp(false, "Invalid or expired token", null, 401);
return $user;
}
5. Rate Limiting
function check_rate_limit($pdo, $user_id, $action, $max = 60) {
$pdo->prepare("DELETE FROM rate_limits
WHERE created_at < NOW() - INTERVAL 1 MINUTE")->execute();
$st = $pdo->prepare("SELECT COUNT(*) FROM rate_limits
WHERE user_id=? AND action=?");
$st->execute([$user_id, $action]);
if ((int)$st->fetchColumn() >= $max) {
resp(false, "Rate limit exceeded", null, 429);
}
$pdo->prepare("INSERT INTO rate_limits (user_id, action)
VALUES (?, ?)")->execute([$user_id, $action]);
}
6. Example Endpoint
// api/orders/create.php
require_once __DIR__ . "/../boot.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") resp(false, "Method not allowed", null, 405);
$user = require_auth($pdo);
check_rate_limit($pdo, $user["id"], "order_create", 10);
$in = get_input();
$product_id = (int)($in["product_id"] ?? 0);
if (!$product_id) resp(false, "product_id required", null, 422);
// ... business logic ...
resp(true, "Order created", ["order_id" => $new_id], 201);
Why No Framework?
- Performance - zero overhead, boot time < 1ms
- Full control - know exactly what happens at every line
- Simple deployment - upload files, done
- Perfect for microservices - small, focused APIs
Conclusion
Native PHP does not mean primitive. With the right patterns (separation of concerns, prepared statements, proper error handling), you can build robust and scalable APIs. This approach is used by the OTPZap API which serves hundreds of requests per minute with consistent sub-50ms response times - without a single framework.