This module covers how to build a RESTful API in PHP running under Apache. We'll implement full CRUD operations using a JSON file for storage.
Try it live:
curl https://cse135.site/php-tutorial/05-rest-api/api.php/items
Building a REST API in PHP is fundamentally different from Node.js because of PHP's per-request execution model:
RESTful URLs like /api/items/42 don't map to PHP files by default. We use Apache's mod_rewrite to route clean URLs to our API script:
RewriteEngine On RewriteRule ^api/items(/.*)?$ api.php$1 [L,QSA]
This sends any request to api/items or api/items/anything to api.php, preserving the path information.
api.php/items and api.php/items/42. Apache passes the part after .php as PATH_INFO automatically. The .htaccess rewrite just makes the URLs cleaner.
Without a framework, PHP routing means reading two things from the request:
$_SERVER['REQUEST_METHOD'] — the HTTP verb (GET, POST, PUT, DELETE)$_SERVER['PATH_INFO'] — the URL path after the script name<?php
// Reading the HTTP method
$method = $_SERVER['REQUEST_METHOD'];
// Reading the path: /items or /items/42
$path = $_SERVER['PATH_INFO'] ?? '/';
// Parse out the resource ID if present
$parts = explode('/', trim($path, '/'));
// $parts[0] = 'items'
// $parts[1] = '42' (or not set for collection requests)
$id = $parts[1] ?? null;
?>
For POST and PUT requests, the client sends JSON in the request body. PHP doesn't auto-parse this like it does for form data:
<?php
// For form data, PHP gives you $_POST
// For JSON, you read the raw body:
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true); // true = associative array
// Now $data['name'] contains the value from {"name": "New Item"}
?>
php://input? PHP's $_POST only works for application/x-www-form-urlencoded and multipart/form-data. For JSON bodies (application/json), you must read the raw input stream.
Here's the full API implementation. The complete file is at api.php.
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Parse request
$method = $_SERVER['REQUEST_METHOD'];
$path = $_SERVER['PATH_INFO'] ?? '/';
$parts = explode('/', trim($path, '/'));
$resource = $parts[0] ?? '';
$id = isset($parts[1]) ? (int)$parts[1] : null;
// Only handle /items routes
if ($resource !== 'items') {
http_response_code(404);
echo json_encode(['error' => 'Not found']);
exit;
}
// Load data from JSON file
$dataFile = __DIR__ . '/items.json';
$items = json_decode(file_get_contents($dataFile), true) ?: [];
?>
<?php
if ($method === 'GET') {
if ($id !== null) {
// GET /items/:id - Find one item
$item = null;
foreach ($items as $i) {
if ($i['id'] === $id) {
$item = $i;
break;
}
}
if (!$item) {
http_response_code(404);
echo json_encode(['error' => 'Item not found']);
exit;
}
echo json_encode($item);
} else {
// GET /items - Return all items
echo json_encode($items);
}
}
?>
<?php
if ($method === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['name'])) {
http_response_code(400);
echo json_encode(['error' => 'Name is required']);
exit;
}
// Generate next ID
$maxId = 0;
foreach ($items as $i) {
if ($i['id'] > $maxId) $maxId = $i['id'];
}
$newItem = [
'id' => $maxId + 1,
'name' => $data['name'],
'completed' => false
];
$items[] = $newItem;
file_put_contents($dataFile, json_encode($items, JSON_PRETTY_PRINT));
http_response_code(201);
echo json_encode($newItem);
}
?>
<?php
if ($method === 'PUT') {
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Item ID required']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$found = false;
foreach ($items as &$item) {
if ($item['id'] === $id) {
if (isset($data['name'])) $item['name'] = $data['name'];
if (isset($data['completed'])) $item['completed'] = (bool)$data['completed'];
$found = true;
$updated = $item;
break;
}
}
unset($item); // break reference
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Item not found']);
exit;
}
file_put_contents($dataFile, json_encode($items, JSON_PRETTY_PRINT));
echo json_encode($updated);
}
?>
<?php
if ($method === 'DELETE') {
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Item ID required']);
exit;
}
$found = false;
$deleted = null;
foreach ($items as $index => $item) {
if ($item['id'] === $id) {
$deleted = $item;
array_splice($items, $index, 1);
$found = true;
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Item not found']);
exit;
}
file_put_contents($dataFile, json_encode($items, JSON_PRETTY_PRINT));
echo json_encode(['message' => 'Item deleted', 'item' => $deleted]);
}
?>
PHP uses http_response_code() to set status codes:
<?php http_response_code(200); // OK (default) http_response_code(201); // Created (after successful POST) http_response_code(400); // Bad Request (invalid input) http_response_code(404); // Not Found (item doesn't exist) http_response_code(405); // Method Not Allowed http_response_code(500); // Internal Server Error ?>
http_response_code() must be called before any output, just like header(). PHP sends headers with the first byte of output.
Cross-origin requests require specific headers. In PHP, set them with header():
<?php
// Allow requests from any origin
header('Access-Control-Allow-Origin: *');
// Allow specific methods
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
// Allow specific headers in requests
header('Access-Control-Allow-Headers: Content-Type');
// Handle the OPTIONS preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit; // Don't process further - just return the headers
}
?>
The browser sends an OPTIONS preflight request before any non-simple request (PUT, DELETE, or requests with custom headers). Your API must respond to it.
# GET all items
curl https://cse135.site/php-tutorial/05-rest-api/api.php/items
# GET one item
curl https://cse135.site/php-tutorial/05-rest-api/api.php/items/1
# POST - Create item
curl -X POST https://cse135.site/php-tutorial/05-rest-api/api.php/items \
-H "Content-Type: application/json" \
-d '{"name": "Learn REST"}'
# PUT - Update item
curl -X PUT https://cse135.site/php-tutorial/05-rest-api/api.php/items/1 \
-H "Content-Type: application/json" \
-d '{"name": "Updated Item", "completed": true}'
# DELETE - Remove item
curl -X DELETE https://cse135.site/php-tutorial/05-rest-api/api.php/items/1
| Aspect | PHP (this tutorial) | Node.js (Express) |
|---|---|---|
| Routing | Manual (PATH_INFO + switch) | Built-in (app.get(), app.post()) |
| JSON body | file_get_contents('php://input') |
req.body (with middleware) |
| Storage | JSON file (must persist externally) | In-memory array (process persists) |
| Status codes | http_response_code(201) |
res.status(201) |
| Response | echo json_encode($data) |
res.json(data) |
| URL params | Parse $_SERVER['PATH_INFO'] |
req.params.id |
| Deployment | Drop files in Apache docroot | Run node server.js + process manager |