Skip to main content
    Courses/PHP/Working with REST APIs

    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()

    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.

    The smallest possible JSON response
    <?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}
    ?>
    Output
    Request:  GET /book.php
    
    Response headers:
      Content-Type: application/json
    
    Response body:
    {"id":1,"title":"PHP for Beginners","inStock":true}
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Branch on the request method
    <?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']);
    }
    ?>
    Output
    Request:  POST /books.php
    
    Response body:
    {"action":"create a book"}
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Read a JSON body, validate, return 201
    <?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
    ]);
    ?>
    Output
    Request:  POST /books.php
    Body:     {"title":"Dune","author":"Herbert"}
    
    Response status: 201 Created
    
    Response body:
    {"id":42,"title":"Dune","author":"Herbert"}
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    One file routing the /books resource (with CORS)
    <?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']);
    }
    ?>
    Output
    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 is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Call an external API two ways
    <?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";
    ?>
    Output
    cURL got HTTP 200: PHP for Beginners
    file_get_contents got: PHP for Beginners
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Now you try. The endpoint below is almost complete — fill in each ___ using the 👉 hint, then check it against the Output panel.

    🎯 Your turn: return a user as JSON
    <?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}
    ?>
    Output
    Request:  GET /user.php
    
    Response body:
    {"id":7,"name":"Sam","admin":false}
    Fill the two ___ 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.

    🎯 Your turn: allow only DELETE
    <?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"}
    ?>
    Output
    Request:  DELETE /books.php?id=1
    
    Response status: 200 OK
    Response body:
    {"deleted":true}
    Fill the blanks so a 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 to text/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 calling header() or http_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 $_POST by mistake. Read the raw body with file_get_contents('php://input') and check the result with if (!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 preflight OPTIONS request with 204. (CORS only affects browsers — curl won'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; set 400/404/405 as 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_ERROR to json_encode/json_decode so bad JSON raises an exception instead of silently returning false/null.

    📋 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

    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.

    🎯 Mini-Challenge: build a GET/POST messages API
    <?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
    ?>
    Handle GET (list two messages), POST (validate a text field, return 201 or 400), and anything else (405). Match the expected responses in the comments.

    🎉 Lesson Complete!

    • ✅ An API returns data, not HTMLheader('Content-Type: application/json') + json_encode()
    • ✅ Branch on $_SERVER['REQUEST_METHOD'] for GET/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 if branches make a router; CORS headers let browsers call you
    • ✅ Consume other APIs with cURL or file_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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service