Building a REST API with Native PHP - No Framework Needed (2026)

Tutorials May 20, 2026 · OTPZap Team

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?

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.