Lesson 18 • Advanced
Working with REST APIs 🔗
By the end of this lesson you'll be able to build a working REST API in plain PHP — reading the HTTP method, parsing a JSON request body, returning JSON with the right status code, routing, adding CORS — and then call an API back from PHP.
What You'll Learn in This Lesson
- Return JSON with header('Content-Type: application/json') and json_encode()
- Branch on the HTTP method using $_SERVER['REQUEST_METHOD']
- Read a JSON request body with php://input and json_decode()
- Set the right status code with http_response_code() (200/201/400/404/405)
- Add a simple router and the CORS headers a browser app needs
- Consume an external API from PHP with cURL and file_get_contents()
$_SERVER and $_GET are new, revisit Forms & User Input first.1️⃣ Returning JSON
A web page sends back HTML; an API (Application Programming Interface) sends back data — almost always as JSON (a plain-text format that looks like a PHP array: {"key":"value"}). Two steps turn any PHP into a JSON response. First, send a header — a behind-the-scenes label on the response — telling the client the body is JSON. Then build a normal array and pass it through json_encode(), which serialises it into a JSON string.
<?php
// A REST API answers in JSON, not HTML. Two lines make any response JSON.
// 1) Tell the browser the body is JSON (this is the "Content-Type" header).
header('Content-Type: application/json');
// 2) Build a normal PHP array, then turn it into a JSON string with json_encode().
$book = [
'id' => 1,
'title' => 'PHP for Beginners',
'inStock' => true,
];
echo json_encode($book); // sends: {"id":1,"title":"PHP for Beginners","inStock":true}
?>Request: GET /book.php
Response headers:
Content-Type: application/json
Response body:
{"id":1,"title":"PHP for Beginners","inStock":true}That's the whole idea of an API response: a PHP array in, a JSON string out, with a Content-Type: application/json header so the caller's code knows to parse it as data rather than display it as text.
2️⃣ HTTP Methods: GET, POST, PUT, DELETE
Every request arrives with an HTTP method (also called a verb) that states the caller's intent. GET reads data, POST creates something new, PUT updates an existing thing, and DELETE removes it. PHP hands you the method in the superglobal $_SERVER['REQUEST_METHOD'] — a built-in array describing the request. You branch on it to decide what to do.
<?php
header('Content-Type: application/json');
// $_SERVER['REQUEST_METHOD'] tells you HOW the client asked: GET, POST, PUT, DELETE.
// You branch on it to decide what to do — this is the core of every REST endpoint.
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
// GET = "read". Send data back.
echo json_encode(['action' => 'read a book']);
} elseif ($method === 'POST') {
// POST = "create". Make a new thing.
echo json_encode(['action' => 'create a book']);
} elseif ($method === 'PUT') {
// PUT = "update". Replace an existing thing.
echo json_encode(['action' => 'update a book']);
} elseif ($method === 'DELETE') {
// DELETE = "remove".
echo json_encode(['action' => 'delete a book']);
} else {
// Anything else isn't allowed on this endpoint.
http_response_code(405); // 405 = Method Not Allowed
echo json_encode(['error' => 'Method not allowed']);
}
?>Request: POST /books.php
Response body:
{"action":"create a book"}Notice the else branch: if a caller uses a method this endpoint doesn't support, you reply 405 Method Not Allowed rather than silently doing the wrong thing. Reporting how it went is the API's job, which is the next section.
3️⃣ Reading the JSON Body & Status Codes
A POST or PUT carries its data in the request body as raw JSON. You can't read that from $_POST (that's only for HTML forms) — instead you read the raw stream php://input with file_get_contents(), then json_decode() it into a PHP array. Always validate the result: json_decode returns null for missing or malformed JSON. A status code is the numeric verdict on the response — set it with http_response_code() before you echo anything.
<?php
header('Content-Type: application/json');
// A POST/PUT request carries its data in the request BODY as raw JSON.
// php://input is the raw body; json_decode turns that JSON string into PHP data.
$raw = file_get_contents('php://input'); // e.g. '{"title":"Dune","author":"Herbert"}'
$data = json_decode($raw, true); // true => decode into an associative array
// ALWAYS validate. json_decode returns null if the body was missing or invalid JSON.
if (!is_array($data) || empty($data['title'])) {
http_response_code(400); // 400 = Bad Request
echo json_encode(['error' => 'A "title" field is required']);
exit; // stop here — don't continue
}
// Input is good — pretend we saved it and return the created record.
http_response_code(201); // 201 = Created
echo json_encode([
'id' => 42,
'title' => $data['title'],
'author' => $data['author'] ?? 'Unknown', // ?? = default if key missing
]);
?>Request: POST /books.php
Body: {"title":"Dune","author":"Herbert"}
Response status: 201 Created
Response body:
{"id":42,"title":"Dune","author":"Herbert"}The flow is read → validate → respond. Invalid input gets 400 Bad Request and an exit so nothing else runs; valid input that creates a record gets 201 Created. The ?? is the null coalescing operator — "use this default if the key is missing".
4️⃣ A Simple Router & CORS
A real endpoint handles a whole resource — for /books that means "list all", "get one by id", "create", and so on. A tiny router reads the method and the id (here from the query string, ?id=1) and dispatches to the right branch. If a browser app on another domain will call your API, you also need CORS headers (Cross-Origin Resource Sharing) — without them the browser blocks the response. Browsers send a preflight OPTIONS request first, which you answer with 204 No Content.
<?php
// A tiny router: one file that handles a whole "/books" resource.
// It reads the METHOD and the id from the URL, then dispatches.
header('Content-Type: application/json');
// CORS: let a browser app on another domain call this API.
header('Access-Control-Allow-Origin: *'); // who may call us (* = anyone)
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); // which methods are allowed
header('Access-Control-Allow-Headers: Content-Type'); // which request headers are allowed
// Browsers send a "preflight" OPTIONS request before a real cross-origin call.
// Answer it with 204 (No Content) and stop.
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
$id = $_GET['id'] ?? null; // /books.php?id=1 -> $id is "1"
// Pretend "database".
$books = [1 => ['id' => 1, 'title' => 'PHP for Beginners']];
if ($method === 'GET' && $id === null) {
echo json_encode(array_values($books)); // list all books
} elseif ($method === 'GET' && isset($books[$id])) {
echo json_encode($books[$id]); // one book
} elseif ($method === 'GET') {
http_response_code(404); // 404 = Not Found
echo json_encode(['error' => 'Book not found']);
} elseif ($method === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
http_response_code(201);
echo json_encode(['id' => 2, 'title' => $data['title'] ?? '']);
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>Request: GET /books.php
Response body:
[{"id":1,"title":"PHP for Beginners"}]
----
Request: GET /books.php?id=99
Response status: 404 Not Found
Response body:
{"error":"Book not found"}This single file is now a working API: list, fetch-by-id with a real 404, create with 201, and a 405 for anything else — all callable from any browser thanks to the CORS headers.
5️⃣ Consuming an API from PHP
APIs talk to each other, so your PHP often needs to call someone else's API. Two built-in tools do the job. cURL is the powerful one: curl_init() opens a request, curl_setopt() configures it (the key option is CURLOPT_RETURNTRANSFER so the body is returned as a string), and curl_exec() fires it. For a simple GET, file_get_contents() fetches a URL in one line. Either way, you json_decode() the response back into a PHP array.
<?php
// The other half of REST: CONSUMING an API from PHP.
// Option A — cURL (built in, full control).
$ch = curl_init('https://api.example.com/books/1');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the body as a string
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE); // the status code, e.g. 200
curl_close($ch);
$book = json_decode($response, true); // JSON string -> PHP array
echo "cURL got HTTP {$status}: {$book['title']}\n";
// Option B — file_get_contents (one line, great for simple GETs).
$raw = file_get_contents('https://api.example.com/books/1');
$book = json_decode($raw, true);
echo "file_get_contents got: {$book['title']}\n";
?>cURL got HTTP 200: PHP for Beginners
file_get_contents got: PHP for BeginnersNow you try. The endpoint below is almost complete — fill in each ___ using the 👉 hint, then check it against the Output panel.
<?php
// 🎯 YOUR TURN — return a single user as JSON.
// Fill in each blank marked ___ , then run it.
// 1) Set the Content-Type header so the client knows it's JSON
___('Content-Type: application/json'); // 👉 the function that sends a header
$user = ['id' => 7, 'name' => 'Sam', 'admin' => false];
// 2) Turn the array into a JSON string and echo it
echo ___($user); // 👉 the function that makes JSON
// ✅ Expected response body:
// {"id":7,"name":"Sam","admin":false}
?>Request: GET /user.php
Response body:
{"id":7,"name":"Sam","admin":false}___ blanks with the header function and the JSON-encoding function, then serve it. The body should be one JSON object.One more. This endpoint should accept only DELETE. Fill in the method key, the verb, and the "not allowed" status code.
<?php
header('Content-Type: application/json');
// 🎯 YOUR TURN — only allow DELETE on this endpoint.
// Fill in the blanks so a DELETE returns 200, and anything else returns 405.
$method = $_SERVER[___]; // 👉 the key that holds the HTTP method
if ($method === ___) { // 👉 the method name, in "quotes"
echo json_encode(['deleted' => true]);
} else {
http_response_code(___); // 👉 the "Method Not Allowed" status code
echo json_encode(['error' => 'Use DELETE']);
}
// ✅ Expected:
// DELETE request -> 200 {"deleted":true}
// GET request -> 405 {"error":"Use DELETE"}
?>Request: DELETE /books.php?id=1
Response status: 200 OK
Response body:
{"deleted":true}DELETE returns 200 and everything else returns 405. Test with curl -X DELETE ....Common Errors (and the fix)
- The browser shows raw JSON or tries to download it — you forgot
header('Content-Type: application/json'), so the response defaults totext/html. Send the JSON content-type header before any output and clients will parse it as data. - "Cannot modify header information — headers already sent" — you echoed something (even a stray space or a blank line before
<?php) before callingheader()orhttp_response_code(). All headers and status codes must come before the first byte of body. Set them at the very top. - $data is null after json_decode — the client sent invalid JSON, or sent a form body so you read
$_POSTby mistake. Read the raw body withfile_get_contents('php://input')and check the result withif (!is_array($data)) { ... }before using it. - Your JavaScript app gets a CORS error in the console — the server didn't send
Access-Control-Allow-Origin. Add the CORS headers, and answer the preflightOPTIONSrequest with204. (CORS only affects browsers —curlwon't show this.) - Everything returns 200 even when it failed — you returned an error message in the body but never called
http_response_code(). Clients check the status code first; set400/404/405as appropriate.
Pro Tips
- 💡 Validate every input. Never trust the body — check required fields exist and have the right type before you touch a database. Most API bugs and security holes start with unvalidated input.
- 💡 Status code first, message second. Machines read the code; humans read the message. Always set the code so callers can branch on it without parsing your text.
- 💡 Add
JSON_THROW_ON_ERRORtojson_encode/json_decodeso bad JSON raises an exception instead of silently returningfalse/null.
📋 Quick Reference — Building a PHP API
| Need | Code | What It Does |
|---|---|---|
| Send JSON | header('Content-Type: application/json'); | Mark the body as JSON |
| Encode | echo json_encode($arr); | PHP array → JSON string |
| Read method | $_SERVER['REQUEST_METHOD'] | GET / POST / PUT / DELETE |
| Read body | file_get_contents('php://input') | Raw JSON request body |
| Decode | json_decode($raw, true); | JSON string → PHP array |
| Status code | http_response_code(201); | Set the HTTP status |
| CORS | header('Access-Control-Allow-Origin: *'); | Allow cross-origin browser calls |
| Call an API | curl_exec($ch); / file_get_contents($url); | Consume another API |
Frequently Asked Questions
Q: What actually makes an API "RESTful"?
REST is a style, not a library. A RESTful API exposes resources (like books or users) at URLs, and uses the HTTP method to say what to do with them: GET to read, POST to create, PUT to update, DELETE to remove. It is stateless (each request carries everything it needs), and it uses standard HTTP status codes to report success or failure. In PHP you build it with plain functions — you read $_SERVER['REQUEST_METHOD'], send JSON, and set the right status code.
Q: Why use php://input instead of $_POST to read the body?
$_POST is only populated for form submissions (Content-Type of application/x-www-form-urlencoded or multipart/form-data). REST clients send a raw JSON body with Content-Type: application/json, which PHP does not parse into $_POST. file_get_contents('php://input') gives you that raw body as a string, and json_decode turns it into PHP data. So for JSON APIs you always read php://input, not $_POST.
Q: Which HTTP status codes should my API return?
The common ones: 200 OK for a successful read or update, 201 Created after a successful POST, 204 No Content when there is nothing to return, 400 Bad Request when the input is invalid, 401/403 for auth problems, 404 Not Found when the resource does not exist, 405 Method Not Allowed for an unsupported method, and 500 for server errors. Set them with http_response_code() before you echo the body, so clients can react without parsing your message text.
Q: What is CORS and why do I need those headers?
CORS (Cross-Origin Resource Sharing) is a browser security rule: by default JavaScript on one domain cannot read a response from a different domain. If your API at api.example.com is called by a page on app.example.com, you must send Access-Control-Allow-Origin so the browser permits it. Browsers also send a preflight OPTIONS request first for non-simple calls — answer it with the allowed methods and headers and a 204 status. Note CORS only affects browsers; cURL and server-to-server calls ignore it.
Q: Should I build a real API on raw PHP like this?
For learning and tiny services, yes — it shows you exactly what is happening. For anything real, reach for a framework or micro-framework (Laravel, Symfony, Slim) once you understand the fundamentals. They give you routing, validation, middleware, and security for free, but every one of them is doing the same things you just learned: reading the method, parsing JSON, setting status codes, and returning JSON.
Mini-Challenge: A Messages Endpoint
No code is filled in this time — just a brief and an outline. Build the endpoint yourself, serve it with php -S localhost:8000, then test each case with curl and check it against the expected output in the comments. This is the read-method → read-body → validate → respond loop you'll use on every real endpoint.
<?php
// 🎯 MINI-CHALLENGE: a tiny "messages" endpoint.
// No code is filled in — work from the steps, then run it.
//
// 1. Send the JSON Content-Type header.
// 2. Read $_SERVER['REQUEST_METHOD'] into a $method variable.
// 3. If $method is "GET": echo a JSON array of two messages
// e.g. [{"id":1,"text":"hi"},{"id":2,"text":"hello"}]
// 4. If $method is "POST":
// - read the JSON body with file_get_contents('php://input') + json_decode
// - if there's no "text" field, send http_response_code(400) and a JSON error
// - otherwise send http_response_code(201) and echo the new message as JSON
// 5. Otherwise: send http_response_code(405) and a JSON error.
//
// ✅ Expected:
// GET -> 200 [{"id":1,"text":"hi"},{"id":2,"text":"hello"}]
// POST {"text":"yo"} -> 201 {"id":3,"text":"yo"}
// POST {} -> 400 {"error":"text is required"}
// your code here
?>text field, return 201 or 400), and anything else (405). Match the expected responses in the comments.🎉 Lesson Complete!
- ✅ An API returns data, not HTML —
header('Content-Type: application/json')+json_encode() - ✅ Branch on
$_SERVER['REQUEST_METHOD']forGET/POST/PUT/DELETE - ✅ Read a JSON body with
file_get_contents('php://input')+json_decode(), then validate it - ✅ Report the outcome with
http_response_code()— 200, 201, 400, 404, 405 - ✅ A few
ifbranches make a router; CORS headers let browsers call you - ✅ Consume other APIs with
cURLorfile_get_contents() - ✅ Next lesson: Building a Micro MVC Framework — structure a real app around these request/response ideas
Sign up for free to track which lessons you've completed and get learning reminders.