REST API

Building a complete CRUD API with PHP routing and JSON responses

curl https://cse135.site/php-tutorial/05-rest-api/api.php/items

The PHP Approach to REST

Building a REST API in PHP is fundamentally different from Node.js because of PHP's per-request execution model:

Node.js REST API:                    PHP REST API:

Browser --> Express (Node.js)        Browser --> Apache --> PHP
               |                                    |       |
          Process runs                       .htaccess rewrites
          continuously                       URL to api.php
               |                                    |
          In-memory data                     Read REQUEST_METHOD
          persists between                   + PATH_INFO
          requests                                  |
               |                             Read/write items.json
          res.json(data)                     (file-based storage)
                                                    |
                                             echo json_encode($data)

URL Rewriting with .htaccess

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.

Routing in PHP

Without a framework, PHP routing means reading two things from the request:

  1. $_SERVER['REQUEST_METHOD'] — the HTTP verb (GET, POST, PUT, DELETE)
  2. $_SERVER['PATH_INFO'] — the URL path after the script name

Reading the JSON Request Body

For POST and PUT requests, the client sends JSON in the request body. PHP does not auto-parse this like it does for form data:

Complete CRUD Implementation

Here is the full API implementation. The complete file is available at api.php.

Setup and Routing

'Not found']); exit; } // Load data from JSON file $dataFile = __DIR__ . '/items.json'; $items = json_decode(file_get_contents($dataFile), true) ?: []; ?>

GET — Read Items

'Item not found']); exit; } echo json_encode($item); } else { // GET /items - Return all items echo json_encode($items); } } ?>

POST — Create Item

'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); } ?>

PUT — Update Item

'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); } ?>

DELETE — Remove Item

'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]); } ?>

Status Codes in PHP

PHP uses http_response_code() to set status codes. Like header(), it must be called before any output:

CORS in PHP

Cross-origin requests require specific headers. In PHP, set them with header():

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 with the appropriate CORS headers.

Testing with curl

# 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

Comparison with Node.js

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

Summary

  • PHP REST APIs run inside Apache, using .htaccess for URL rewriting and PATH_INFO for routing
  • Manual routing reads REQUEST_METHOD and PATH_INFO from $_SERVER
  • JSON request bodies require file_get_contents('php://input') and json_decode()
  • Use http_response_code() for status codes and header() for CORS
  • PHP needs external storage (files, database) because each request starts fresh