intermediate
Step 12 of 16
Building a REST API
PHP Programming
Building a REST API
Building a REST API is one of the most common tasks in PHP development. A well-structured API receives HTTP requests, routes them to the appropriate handler, validates input, interacts with the database, and returns JSON responses with proper status codes. In this lesson, you will build a simple but well-structured REST API from scratch using plain PHP with PSR-4 autoloading, learning the patterns that frameworks like Laravel and Symfony use internally.
Simple Router
<?php
// src/Router.php
namespace App;
class Router {
private array $routes = [];
public function get(string $path, callable|array $handler): void {
$this->addRoute('GET', $path, $handler);
}
public function post(string $path, callable|array $handler): void {
$this->addRoute('POST', $path, $handler);
}
public function put(string $path, callable|array $handler): void {
$this->addRoute('PUT', $path, $handler);
}
public function delete(string $path, callable|array $handler): void {
$this->addRoute('DELETE', $path, $handler);
}
private function addRoute(string $method, string $path, callable|array $handler): void {
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $path);
$this->routes[] = [
'method' => $method,
'pattern' => "#^{$pattern}$#",
'handler' => $handler
];
}
public function dispatch(string $method, string $uri): mixed {
foreach ($this->routes as $route) {
if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) {
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
$handler = $route['handler'];
if (is_array($handler)) {
[$class, $method] = $handler;
$controller = new $class();
return $controller->$method(...$params);
}
return $handler(...$params);
}
}
http_response_code(404);
return json_encode(['error' => 'Route not found']);
}
}
?>
Controller
<?php
// src/Controllers/UserController.php
namespace App\Controllers;
use App\Database;
class UserController {
private function jsonResponse(mixed $data, int $status = 200): string {
http_response_code($status);
header('Content-Type: application/json');
return json_encode($data, JSON_PRETTY_PRINT);
}
private function getInput(): array {
return json_decode(file_get_contents('php://input'), true) ?? [];
}
public function index(): string {
$users = Database::fetchAll("SELECT id, name, email, role FROM users ORDER BY name");
return $this->jsonResponse(['data' => $users]);
}
public function show(string $id): string {
$user = Database::fetchOne("SELECT id, name, email, role FROM users WHERE id = ?", [(int)$id]);
if (!$user) {
return $this->jsonResponse(['error' => 'User not found'], 404);
}
return $this->jsonResponse(['data' => $user]);
}
public function store(): string {
$input = $this->getInput();
$errors = [];
if (empty($input['name'])) $errors[] = 'Name is required';
if (empty($input['email']) || !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Valid email is required';
}
if (!empty($errors)) {
return $this->jsonResponse(['errors' => $errors], 422);
}
Database::query(
"INSERT INTO users (name, email, role) VALUES (?, ?, ?)",
[$input['name'], $input['email'], $input['role'] ?? 'user']
);
$id = Database::getConnection()->lastInsertId();
$user = Database::fetchOne("SELECT * FROM users WHERE id = ?", [$id]);
return $this->jsonResponse(['data' => $user, 'message' => 'User created'], 201);
}
public function update(string $id): string {
$input = $this->getInput();
$user = Database::fetchOne("SELECT * FROM users WHERE id = ?", [(int)$id]);
if (!$user) {
return $this->jsonResponse(['error' => 'User not found'], 404);
}
Database::query(
"UPDATE users SET name = ?, email = ? WHERE id = ?",
[$input['name'] ?? $user['name'], $input['email'] ?? $user['email'], $id]
);
return $this->jsonResponse(['message' => 'User updated']);
}
public function destroy(string $id): string {
Database::query("DELETE FROM users WHERE id = ?", [(int)$id]);
return $this->jsonResponse(null, 204);
}
}
?>
Entry Point
<?php
// public/index.php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Router;
use App\Controllers\UserController;
// CORS headers (for frontend access)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$router = new Router();
// Define routes
$router->get('/api/users', [UserController::class, 'index']);
$router->get('/api/users/{id}', [UserController::class, 'show']);
$router->post('/api/users', [UserController::class, 'store']);
$router->put('/api/users/{id}', [UserController::class, 'update']);
$router->delete('/api/users/{id}', [UserController::class, 'destroy']);
// Dispatch request
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
echo $router->dispatch($method, $uri);
?>
Pro tip: This manual routing approach teaches you what frameworks do under the hood. In production, use a framework like Laravel or Slim that provides battle-tested routing, middleware, validation, and ORM integration. Understanding these fundamentals will make you far more effective with any framework because you will understand the underlying patterns.
Key Takeaways
- REST APIs use HTTP methods (GET, POST, PUT, DELETE) mapped to CRUD operations on resources.
- Always validate and sanitize input, return proper HTTP status codes, and set Content-Type headers.
- Use a router to map URLs to controller methods, with support for URL parameters like
/users/{id}. - Handle CORS headers for frontend access and OPTIONS preflight requests.
- Separate concerns: routing, controllers, database access, and validation should be in different classes.