Working with REST APIs

Reviewed & published by Brayan K

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.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

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}
?>

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.
// A web server sets it from the request line; on the command line, set it yourself:
$_SERVER['REQUEST_METHOD'] = 'POST';   // pretend this request was POST /books.php

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

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"}'
// Off a web server that body is empty, so stand in for the client here:
$raw  = $raw ?: '{"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
]);
?>

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;
}

// Again: a server fills these two from the request. This stands in for
// GET /books.php with no id, which is the "list them all" branch.
$_SERVER['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'GET';

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

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";
?>

Now 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}
?>

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"}
?>

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Building a PHP API

NeedCodeWhat It Does
Send JSONheader('Content-Type: application/json');Mark the body as JSON
Encodeecho json_encode($arr);PHP array → JSON string
Read method$_SERVER['REQUEST_METHOD']GET / POST / PUT / DELETE
Read bodyfile_get_contents('php://input')Raw JSON request body
Decodejson_decode($raw, true);JSON string → PHP array
Status codehttp_response_code(201);Set the HTTP status
CORSheader('Access-Control-Allow-Origin: *');Allow cross-origin browser calls
Call an APIcurl_exec($ch); / file_get_contents($url);Consume another API

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
?>

🎉 Lesson Complete!

Practice quiz

Which two steps turn PHP output into a JSON API response?

  • echo the array, then call header()
  • Call json_decode(), then print
  • Send header('Content-Type: application/json'), then echo json_encode($array)
  • Set http_response_code(200) and echo HTML

Answer: Send header('Content-Type: application/json'), then echo json_encode($array). Send the JSON content-type header, then build a PHP array and pass it through json_encode() to serialise it into a JSON string.

Which superglobal tells you the HTTP method of the request?

  • $_SERVER['REQUEST_METHOD']
  • $_GET['method']
  • $_POST['verb']
  • $_REQUEST['type']

Answer: $_SERVER['REQUEST_METHOD']. $_SERVER['REQUEST_METHOD'] holds GET/POST/PUT/DELETE; you branch on it to decide what the endpoint does.

Which REST verb is used to create a new resource?

  • GET
  • PUT
  • DELETE
  • POST

Answer: POST. GET reads, POST creates, PUT updates an existing thing, and DELETE removes it.

Why read php://input instead of $_POST for a JSON API?

  • $_POST is deprecated
  • $_POST is only populated for form submissions, not a raw JSON body
  • php://input is faster
  • $_POST cannot hold strings

Answer: $_POST is only populated for form submissions, not a raw JSON body. $_POST is filled only for form-encoded bodies; REST clients send raw JSON, so you read php://input and json_decode it.

After json_decode() of the request body, what must you always do?

  • Validate it — json_decode returns null for missing or invalid JSON
  • Immediately save it to the database
  • Re-encode it
  • Send it back unchanged

Answer: Validate it — json_decode returns null for missing or invalid JSON. json_decode returns null on missing/malformed JSON, so check the result (e.g. is_array and required fields) before using it.

Which status code should a successful POST that creates a record return?

  • 200 OK
  • 204 No Content
  • 201 Created
  • 404 Not Found

Answer: 201 Created. A successful creation returns 201 Created; set it with http_response_code(201) before echoing the new record.

Which status code means the request body was invalid?

  • 405 Method Not Allowed
  • 400 Bad Request
  • 201 Created
  • 500 Server Error

Answer: 400 Bad Request. Invalid input gets 400 Bad Request (often followed by exit), so nothing else runs on a bad body.

When must http_response_code() and header() be called?

  • After echoing the JSON body
  • Only inside json_encode()
  • At the very end of the script
  • Before any output — once a byte is sent, headers can't be changed

Answer: Before any output — once a byte is sent, headers can't be changed. Headers and status codes must be set before the first byte of output, or you get 'headers already sent'.

What is CORS and which header enables a cross-origin browser call?

  • A caching rule; Cache-Control
  • A browser security rule; Access-Control-Allow-Origin
  • An auth scheme; Authorization
  • A compression setting; Content-Encoding

Answer: A browser security rule; Access-Control-Allow-Origin. CORS is a browser rule that blocks cross-domain reads by default; Access-Control-Allow-Origin permits it (cURL ignores CORS).

Which cURL option makes curl_exec() return the response body as a string?

  • CURLOPT_POST
  • CURLOPT_HEADER
  • CURLOPT_RETURNTRANSFER
  • CURLOPT_URL

Answer: CURLOPT_RETURNTRANSFER. Set CURLOPT_RETURNTRANSFER to true so curl_exec() returns the body as a string instead of printing it; then json_decode it.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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.

Related lessons