REST API in PHP Without a Framework: Build It in 6 Steps

Santaji GadePHPDevelopmentYesterday10 Views

rest api in php

A practical guide to a rest api in php without a framework, covering routing, PDO backed models, bearer token authentication, and JSON error handling.

Development PHP REST API JSON

Installing a full framework to serve a handful of JSON endpoints is a lot of weight for a small job. A REST API in PHP without a framework needs only a router, a database connection, and a clear idea of what each HTTP verb should do.

01

Why Build a REST API in PHP Without a Framework

A framework earns its weight on a large application with dozens of routes, middleware, and moving parts. Roy Fielding's original REST dissertation never mentions a framework at all, since the architecture itself is just a set of constraints over plain HTTP.

A small internal tool, a mobile app backend with six endpoints, or a proof of concept rarely needs any of that structure.

Plain PHP with a thin routing layer starts in minutes and stays easy to read months later, since nothing hides behind a framework's own conventions.

Everything below is production usable code, not a toy example, built around the same HTTP rules a framework would enforce anyway.

02

Setting Up a Front Controller

Every request routes through a single PHP file, which keeps the routing logic in one place instead of scattered across separate files per endpoint.

.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]

Every request that does not point at a real file gets forwarded to index.php, which becomes the single entry point the router below reads from. mod_rewrite handles this on Apache, and Nginx uses an equivalent try_files directive in its own server block.

03

Building a Simple Router Class

A router just needs to match a method and a path pattern against the current request, then call the matching handler.

Router.php
final class Router {
    private array $routes = [];

    public function add(string $method, string $pattern, callable $handler): void {
        $this->routes[] = ['method' => $method, 'pattern' => $pattern, 'handler' => $handler];
    }

    public function dispatch(string $method, string $path): void {
        foreach ($this->routes as $route) {
            if ($route['method'] !== $method) {
                continue;
            }
            $regex = '#^' . preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $route['pattern']) . '$#';
            if (preg_match($regex, $path, $matches)) {
                $route['handler'](array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY));
                return;
            }
        }
        http_response_code(404);
        echo json_encode(['error' => 'Route not found']);
    }
}

Turning {id} style placeholders into a named regex group lets a single pattern like /posts/{id} match /posts/42 and hand back id already extracted.

04

Returning Proper JSON Responses and Status Codes

A REST API's status code carries real meaning, and a client integrating against it depends on that meaning being consistent. The full list of codes and what each one is meant to signal comes from MDN's HTTP status reference, which is worth keeping open while building any API.

respond.php
function json_response(array $data, int $status = 200): void {
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data, JSON_UNESCAPED_SLASHES);
    exit;
}
HTTP MethodActionTypical Status Code
GETRead one or many resources200 on success, 404 if missing
POSTCreate a new resource201 on success
PUTReplace an existing resource200 on success
DELETERemove a resource204 on success
05

Handling GET, POST, PUT, and DELETE for a Resource

Registering routes for a single resource follows the same shape regardless of how many resources the API eventually grows to cover, and every response still follows the same JSON structure a client already expects.

index.php
$router = new Router();

$router->add('GET', '/posts', fn() => json_response(get_all_posts($pdo)));
$router->add('GET', '/posts/{id}', fn($p) => json_response(get_post($pdo, (int) $p['id'])));
$router->add('POST', '/posts', fn() => json_response(create_post($pdo, get_json_body()), 201));
$router->add('PUT', '/posts/{id}', fn($p) => json_response(update_post($pdo, (int) $p['id'], get_json_body())));
$router->add('DELETE', '/posts/{id}', fn($p) => json_response(delete_post($pdo, (int) $p['id']), 204));

$router->dispatch($_SERVER['REQUEST_METHOD'], parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

Five lines wire up a complete resource, and adding a second resource means five more lines rather than a new controller class and configuration file.

06

Connecting Routes to a PDO Backed Model

The handler functions referenced above stay small, since each one wraps a single PDO prepared statement.

posts_model.php
function get_post(PDO $pdo, int $id): array {
    $stmt = $pdo->prepare('SELECT id, title, body FROM posts WHERE id = ?');
    $stmt->execute([$id]);
    $post = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$post) {
        json_response(['error' => 'Post not found'], 404);
    }
    return $post;
}

Calling json_response directly from inside the model and letting it exit keeps the missing resource case short, without an extra layer of exceptions just to bubble a 404 upward.

07

Authenticating Requests With a Bearer Token

A write endpoint left open to anyone is a real risk the moment the API is reachable from outside a local network. The Bearer scheme is a standard HTTP authentication pattern, not something specific to this guide.

auth_middleware.php
function require_bearer_token(): void {
    $header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

    if (!preg_match('/^Bearer\s+(.+)$/', $header, $matches)) {
        json_response(['error' => 'Missing or malformed token'], 401);
    }
    if (!hash_equals(getenv('API_TOKEN'), $matches[1])) {
        json_response(['error' => 'Invalid token'], 403);
    }
}

hash_equals() compares strings in constant time, which matters here since a plain === comparison can leak timing information about how much of the token matched. RFC 6750 defines the Bearer token scheme this pattern follows.

08

Handling Errors With a Consistent JSON Format

A client integrating against the API should be able to parse every error the same way, regardless of which endpoint produced it.

error_handler.php
set_exception_handler(function (Throwable $e) {
    json_response([
        'error' => 'Server error',
        'request_id' => bin2hex(random_bytes(8))
    ], 500);
});

Returning a random request_id instead of the raw exception message keeps internal details out of the response while still giving support a value to search server logs for.

  • Validate the request body before touching the database: the JSON decoding and validation rules from earlier in this series apply directly here.
  • Return the right status code, not just 200: a client relies on the code as much as the body to know what happened.
  • Never echo a raw exception message: it can leak file paths, query fragments, or other internal details to a caller.
  • Rate limit write endpoints: the same IP based limiter used for a contact form works just as well in front of a POST route.
  • Check requests against the OWASP API Security Top 10: broken authentication and excessive data exposure are the two most common issues in a hand rolled API.
  • Version the API in the URL path: a prefix like /v1/posts leaves room to change the response shape later without breaking existing clients.
09

Testing the API With curl

A quick command line request using curl confirms the whole chain works before any client application gets involved.

terminal
# create a post
curl -X POST https://api.example.com/posts \
  -H "Authorization: Bearer your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello","body":"First post"}'

A successful call returns status 201 along with the newly created row, confirming the router, the model, and the authentication check all worked together correctly.

10

Watch: Building a PHP REST API From Scratch

Dave Hollingworth builds a working REST API using plain, object oriented PHP and MySQL, which follows a very similar shape to the router and model pattern in this guide.

Video credit: Dave Hollingworth.

Swapping the video's approach for the named route parameters and bearer token check in this guide is a natural next step once the base pattern feels familiar.

HTTP Request Router Bearer Auth PDO Model JSON Response

Frequently Asked Questions

For a small, focused API it often is. Once the number of resources and business rules grows large, the structure a framework provides usually becomes worth the added weight.

Faster setup, fewer dependencies to keep updated, and a codebase where every line of routing and request handling is visible instead of hidden behind configuration.

It is a reasonable baseline for a server to server integration, though a public facing API serving many users usually needs a fuller OAuth style flow eventually.

Yes. Always setting an explicit status code, even 200, avoids relying on PHP's default, which can vary depending on what already ran earlier in the request.

Add a new model file with its own functions and register a matching set of routes, following the exact same five line pattern used for posts in this guide.

What We Learn Today

1

A front controller centralizes routing

Every request passes through one file instead of many scattered endpoints.

2

Status codes carry real meaning

Clients depend on the code, not just the response body, to know what happened.

3

Route registration stays short

Five lines wire up a full resource without any generated boilerplate.

4

Bearer tokens need constant time checks

hash_equals prevents a timing attack a plain comparison would allow.

5

Errors should never leak internals

A request ID beats a raw exception message in every response.

6

curl confirms the whole chain

A single command line request tests routing, auth, and the model together.

Ready to Ship a Lightweight PHP API?

Pair a rest api in php without a framework with a wider development workflow to keep every endpoint fast, secure, and easy to extend.

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Loading Next Post...
Search
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...