Build REST API in PHP Using GET, POST, PUT & DELETE

1433 views
Build a Simple REST API in PHP – Full Guide with GET, POST, PUT & DELETE Methods

REST (Representational State Transfer) is an architectural style for web services that uses standard HTTP methods (GET, POST, PUT, DELETE, etc.) to operate on resources via URIs. This tutorial demonstrates how to build a simple REST API in pure PHP (no frameworks) supporting the four main methods: GET, POST, PUT, DELETE.

Why is this useful for beginners? Because it shows the core mechanics of REST in the simplest form: handling requests, sending JSON responses, and processing input. Once you understand the basics here, you can scale it up (with databases, authentication, frameworks, etc.).

 

1. GET Method

The GET method is typically used to read or retrieve resources.

Code example

Here’s how the tutorial shows it:

 

<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$data = array(
    array('id' => '1', 'name' => 'Bandung'),
    array('id' => '2', 'name' => 'Jakarta'),
    array('id' => '3', 'name' => 'Surabaya'),
);

// optional search query parameter
if (!empty($_GET['search'])) {
    $key = array_search($_GET['search'], array_column($data, 'name'), true);
    $id = $data[$key]['id'];
    $name = $data[$key]['name'];
    $result = array('id' => $id, 'name' => $name, 'status' => 'success');
} else {
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'][] = 'success';
}

http_response_code(200);
echo json_encode($result);
?>

 

Explanation & tips

  • The headers at the top set CORS (Access‐Control‐Allow‐Origin: *) so any origin can call this API, set response content type to JSON, and restrict allowed HTTP method to GET.
  • $data is a hard-coded array representing cities. In a real app this would come from a database.
  • The script checks if $_GET[‘search’] is provided: if yes, it searches the name field in the data and returns only the matching city. If not, it returns the full list.
  • Response sent with http_response_code(200) and echo json_encode($result);
  • For beginners: you should sanitize and validate input when moving to real data sources (databases).

 

2. POST Method

POST is used to create new resources.

Code example

From the tutorial:

 

 <?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$data = array(
    array('id' => '1', 'name' => 'Bandung'),
    array('id' => '2', 'name' => 'Jakarta'),
    array('id' => '3', 'name' => 'Surabaya'),
);

if (!empty($_POST['name']) && !empty($_POST['id'])) {  // new data input
    $newdata = array('id' => $_POST['id'], 'name' => $_POST['name']);
    $data[] = $newdata;
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'] = 'success';
} else {
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'] = 'success';
}

http_response_code(200);
echo json_encode($result);
?>

 

Explanation & tips

  • Headers allow POST method specifically.
  • Again using $data as initial set.
  • Checks for $_POST[‘id’] and $_POST[‘name’]. If present, it appends a new item to the $data array. Then returns the full list including the new one.
  • In a real application you’d insert into a database. You’d also check for duplicates, enforce data types, return appropriate errors (e.g., 400 Bad Request if missing fields).
  • For testing you can use a tool like Postman (as the article mentions) to send a POST request with form-data or JSON.

 

3. PUT Method

PUT is used to update an existing resource.

Code example

 

<?php
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: PUT");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");

$data = array(
    array('id' => '1', 'name' => 'Bandung'),
    array('id' => '2', 'name' => 'Jakarta'),
    array('id' => '3', 'name' => 'Surabaya'),
);

$method = $_SERVER['REQUEST_METHOD'];
if ('PUT' === $method) {
    parse_str(file_get_contents('php://input'), $_PUT);
}

if (!empty($_PUT['id']) && !empty($_PUT['name'])) {
    foreach ($data as &$value) {
        if ($value['id'] === $_PUT['id']) {
            $value['name'] = $_PUT['name'];
            break;
        }
    }
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'] = 'success';
} else {
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'] = 'success';
}

http_response_code(200);
echo json_encode($result);
?>

  

 

Explanation & tips

  • Uses Access-Control-Allow-Methods: PUT which signals the API is willing to accept PUT requests.
  • Because PHP doesn’t automatically populate $_PUT, the code uses parse_str(file_get_contents(‘php://input’), $_PUT) to convert the raw request body into an array.
  • It locates the entry in $data matching the id from $_PUT, and updates the name.
  • Then returns the full updated list with status success.
  • In real apps: use the right HTTP status codes (e.g., 404 if resource not found, 400 if bad input). Also validate that id exists before updating.
  • In restful design: PUT is often idempotent (calling it multiple times with same data yields same effect).

 

4. DELETE Method

DELETE is used to delete resources.

Code example

 

<?php
header("Access-Control-Allow‐Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow‐Methods: DELETE");
header("Access-Control-Allow‐Headers: Content‐Type, Access‐Control-Allow‐Headers, Authorization, X-Requested-With");

$data = array(
    array('id' => '1', 'name' => 'Bandung'),
    array('id' => '2', 'name' => 'Jakarta'),
    array('id' => '3', 'name' => 'Surabaya'),
);

$method = $_SERVER['REQUEST_METHOD'];
if ('DELETE' === $method) {
    parse_str(file_get_contents('php://input'), $_DELETE);
}

if (!empty($_DELETE['id'])) {
    foreach ($data as $d) {
        if ($d['id'] != $_DELETE['id']) {
            $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
        }
    }
    $result['status'] = 'success';
} else {
    foreach ($data as $d) {
        $result['city'][] = array('id' => $d['id'], 'name' => $d['name']);
    }
    $result['status'] = 'success';
}

http_response_code(200);
echo json_encode($result);
?>

  

Explanation & tips

  • Sets header to allow DELETE method.
  • Uses file_get_contents(‘php://input’) + parse_str() to read the body (since $_DELETE isn’t built‐in).
  • Checks for id to delete; then filters out the matching item from $data.
  • Returns the updated list of cities.
  • In real apps: you’d check whether the id exists, then delete in the database, respond with 204 No Content or 200 OK and maybe send minimal body. Also handle errors (404, 403, etc.).

 

The tutorial ends by summarizing that the four methods (GET, POST, PUT, DELETE) cover the core CRUD (Create, Read, Update, Delete) operations via REST API in PHP.

Why it’s a good starting point

  • Shows the fundamental mechanics of REST with PHP without the “magic” of a framework.
  • Provides simple, readable code examples that you can adapt.
  • Helps you understand how HTTP methods correlate to CRUD operations and how to handle different request method types in PHP.

What you should do next

  • Replace the static $data array with actual database queries (MySQL, PostgreSQL, etc.).
  • Use prepared statements to prevent SQL injection.
  • Use proper HTTP status codes for errors and success responses (e.g., 201 Created, 204 No Content for DELETE, 400 Bad Request, 404 Not Found).
  • Add request validation (is the id valid? is the name present?).
  • Consider authentication/authorization (e.g., API keys, JWT tokens) so only authorized users can change data.
  • Accept JSON input (via json_decode(file_get_contents(‘php://input’), true)) instead of form data for POST/PUT/DELETE.
  • Structure your code to separate routing, controller logic, and database logic (even if still in plain PHP).
  • Add documentation (OpenAPI/Swagger) so clients can understand and test your API.

 

If you’re a beginner PHP developer aiming to build RESTful APIs, this tutorial is an excellent first step. It strips away complexity and shows you how the pieces fit together. Once you’re comfortable with this, you can scale up to frameworks (e.g., Laravel with api.php routes, controllers, resources) or micro-frameworks (e.g., Lumen, Slim), but the core ideas remain the same.

I highly recommend downloading the example files (the author provides a GitHub link) and experimenting: add new endpoints, swap in database logic, handle JSON body input, etc. As you evolve the tutorial code, you’ll build confidence to design real APIs for production.

 

Plenty of “REST API in PHP” tutorials show you four separate scripts, each holding a hardcoded array that resets on every request. That’s a fine five-minute demo, but it skips the two things that actually make an API usable: real persistence and correctly parsed request bodies. Get those two things wrong and the tutorial code won’t survive contact with a real client — Postman will work, then your JS fetch() call won’t, and you’ll spend an hour confused about why.

This guide builds the same CRUD API — cities, in this case — but with a single routed entry point, a real MySQL table behind it, and request parsing that works whether the client sends JSON (the modern default) or form-encoded data.

The Core Problem with the “Quick Version”

Two shortcuts show up constantly in beginner PHP REST tutorials, and both cause real bugs:

  1. In-memory arrays. If your “database” is a PHP array declared at the top of the script, every request starts from scratch. Your POST looks like it worked because the response echoes the array plus your new item — but nothing was saved. Refresh, and it’s gone.
  2. parse_str(file_get_contents(‘php://input’)) for PUT/DELETE bodies. This only decodes application/x-www-form-urlencoded bodies (id=2&name=Pune). The moment a client sends Content-Type: application/json — which is what fetch(), Axios, and most modern frontends do by default — parse_str() silently returns nothing useful, and your update quietly does nothing.

Both are easy to fix. Here’s the corrected version.

 

Step 1: A Real Table Instead of a Hardcoded Array

CREATE TABLE cities (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

INSERT INTO cities (name) VALUES ('Bandung'), ('Jakarta'), ('Surabaya');

And a small PDO connection file, db.php:

<?php
function get_db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=cities_api;charset=utf8mb4',
            getenv('DB_USER') ?: 'root',
            getenv('DB_PASS') ?: '',
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
    }
    return $pdo;
}

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION matters more than it looks like it does: without it, a failed query returns false instead of throwing, and silent failures are far harder to debug than a thrown exception you can catch and log.

 

Step 2: One Entry Point, Routed by Method and Path

Instead of four separate files, use a single index.php and let $_SERVER[‘REQUEST_METHOD’] and the URL path decide what runs. This is the shape every framework’s router is doing under the hood — seeing it in plain PHP makes frameworks a lot less mysterious later.

<?php
require 'db.php';

header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *'); // tighten this to a specific origin in production
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Handle CORS preflight requests and exit early.
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

function json_response(int $status, array $body): void {
    http_response_code($status);
    echo json_encode($body);
    exit;
}

// Read and decode the body ONCE, correctly, regardless of method.
// Works for JSON (the common case) and falls back to form-encoded data.
function get_request_body(): array {
    $raw = file_get_contents('php://input');
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';

    if (str_contains($contentType, 'application/json')) {
        $decoded = json_decode($raw, true);
        return json_last_error() === JSON_ERROR_NONE && is_array($decoded) ? $decoded : [];
    }

    parse_str($raw, $parsed);
    return $parsed;
}

$method = $_SERVER['REQUEST_METHOD'];
$path   = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
$segments = explode('/', $path);
$id = isset($segments[1]) && ctype_digit($segments[1]) ? (int) $segments[1] : null;

$pdo = get_db();

switch ($method) {
    case 'GET':
        handle_get($pdo, $id);
        break;
    case 'POST':
        handle_post($pdo, get_request_body());
        break;
    case 'PUT':
        handle_put($pdo, $id, get_request_body());
        break;
    case 'DELETE':
        handle_delete($pdo, $id);
        break;
    default:
        json_response(405, ['status' => 'error', 'message' => 'Method not allowed']);
}

The get_request_body() helper is the single fix that matters most in this whole article: it checks Content-Type and decodes JSON when that’s what was sent, instead of assuming form encoding. Every handler below calls it once and gets a plain associative array either way.

 

Step 3: GET — Read One or Many, with Real Status Codes

function handle_get(PDO $pdo, ?int $id): void {
    if ($id !== null) {
        $stmt = $pdo->prepare('SELECT id, name FROM cities WHERE id = ?');
        $stmt->execute([$id]);
        $city = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$city) {
            json_response(404, ['status' => 'error', 'message' => "City {$id} not found"]);
        }
        json_response(200, ['status' => 'success', 'data' => $city]);
    }

    // Optional ?search= query param, same idea as the original tutorial, done safely.
    if (!empty($_GET['search'])) {
        $stmt = $pdo->prepare('SELECT id, name FROM cities WHERE name LIKE ?');
        $stmt->execute(['%' . $_GET['search'] . '%']);
    } else {
        $stmt = $pdo->query('SELECT id, name FROM cities ORDER BY id');
    }

    json_response(200, ['status' => 'success', 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
}

Note the prepared statement even for a LIKE search — string-concatenating user input into SQL is exactly how the original array-based version would break the moment it got hooked up to a real database.

 

Step 4: POST — Create, with Input Validation

function handle_post(PDO $pdo, array $body): void {
    $name = trim($body['name'] ?? '');

    if ($name === '') {
        json_response(400, ['status' => 'error', 'message' => 'Field "name" is required']);
    }

    $stmt = $pdo->prepare('INSERT INTO cities (name) VALUES (?)');
    $stmt->execute([$name]);

    json_response(201, [
        'status' => 'success',
        'data' => ['id' => (int) $pdo->lastInsertId(), 'name' => $name],
    ]);
}

 

Two deliberate differences from the “return the whole list back” pattern: it returns 201 Created (the correct status for a successful creation, not 200), and the response body is just the new resource — not the entire table. A client creating one city doesn’t need the other thousand rows echoed back.

 

Step 5: PUT — Update, Checking Existence First

function handle_put(PDO $pdo, ?int $id, array $body): void {
    if ($id === null) {
        json_response(400, ['status' => 'error', 'message' => 'City ID is required in the URL, e.g. /cities/2']);
    }

    $name = trim($body['name'] ?? '');
    if ($name === '') {
        json_response(400, ['status' => 'error', 'message' => 'Field "name" is required']);
    }

    $stmt = $pdo->prepare('UPDATE cities SET name = ? WHERE id = ?');
    $stmt->execute([$name, $id]);

    if ($stmt->rowCount() === 0) {
        json_response(404, ['status' => 'error', 'message' => "City {$id} not found"]);
    }

    json_response(200, ['status' => 'success', 'data' => ['id' => $id, 'name' => $name]]);
}

rowCount() === 0 is doing real work here — it’s how you tell the difference between “updated successfully” and “that ID doesn’t exist,” which the original tutorial’s version couldn’t distinguish (it always returned 200 regardless).

 

Step 6: DELETE — Remove, Return 204

function handle_delete(PDO $pdo, ?int $id): void {
    if ($id === null) {
        json_response(400, ['status' => 'error', 'message' => 'City ID is required in the URL, e.g. /cities/2']);
    }

    $stmt = $pdo->prepare('DELETE FROM cities WHERE id = ?');
    $stmt->execute([$id]);

    if ($stmt->rowCount() === 0) {
        json_response(404, ['status' => 'error', 'message' => "City {$id} not found"]);
    }

    http_response_code(204); // No Content — correct for a successful delete with no body
    exit;
}

 

204 No Content is the conventional response for a successful DELETE. Returning a body with 200 isn’t wrong, but a REST-conventional client expects an empty response here.

Calling It from the Client Side

With ID-based routing and JSON body parsing in place, the API behaves the way a fetch()-based frontend actually expects:

// Create
await fetch('/index.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Pune' }),
});

// Update
await fetch('/index.php/2', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Bengaluru' }),
});

// Delete
await fetch('/index.php/2', { method: 'DELETE' });

 

No form-encoding workarounds needed — the server reads the Content-Type header and decodes accordingly.

 

Where This Still Falls Short of Production

This version fixes the two bugs that break as soon as you leave the demo, but a few things are still worth layering on before this sits on the open internet:

  • Authentication — right now, anyone can call any endpoint. A simple API key checked via an Authorization header, or JWT for anything user-scoped, is the next step.
  • Rate limiting — a public write endpoint with no throttling is an open invitation for abuse; even a basic per-IP counter in Redis or a database table helps.
  • Restrict Access-Control-Allow-Origin — the wildcard * is fine for local testing, but a production API serving a specific frontend should name that origin explicitly.
  • PATCH vs. PUT — this guide follows the common convention where PUT replaces a resource; if you need partial updates (change just one field without resending the whole object), add a PATCH handler that only updates the fields present in the body.
  • Pagination — GET returning every row works fine for three cities, not for three million; add LIMIT/OFFSET or cursor-based pagination once the table grows.
  • Back the API with a real table and prepared statements — a hardcoded array can’t survive a page refresh, let alone a second server.
  • Parse the request body based on Content-Type, not parse_str() alone — otherwise JSON clients silently fail.
  • Return the HTTP status that matches what happened: 201 for created, 204 for deleted, 404 when the ID doesn’t exist, 400 for bad input. A REST API that always returns 200 isn’t really RESTful, just JSON-over-HTTP.
  • Route through one entry point rather than one file per verb — it’s the same pattern every framework’s router uses, just visible.

 

Frequently Asked Questions

+

What is CRUD in REST API?

CRUD in REST API refers to the four basic operations performed on data: Create – Add new data (POST) Read – Fetch existing data (GET) Update – Modify existing data (PUT / PATCH) Delete – Remove data (DELETE) In a REST API, each CRUD operation is mapped to an HTTP method, making APIs easy to understand and maintain.
+

Which HTTP method is used for update?

The PUT HTTP method is commonly used to update existing resources in a REST API. PUT replaces the entire resource. PATCH is used for partial updates.
+

How to secure PHP REST API?

To secure a PHP REST API, follow these best practices: Use authentication (API keys, JWT, or OAuth) Always use HTTPS Validate and sanitize all inputs Implement authorization (role-based access) Use rate limiting to prevent abuse Protect against SQL injection using prepared statements Return proper HTTP status codes and avoid exposing sensitive errors Securing your API ensures data protection and prevents unauthorized access.
+

What is a REST API in PHP?

A REST API in PHP is a web service that follows REST (Representational State Transfer) principles, allowing applications to communicate over HTTP using methods such as GET, POST, PUT, and DELETE. It commonly exchanges data in JSON format.
+

Why should I build a REST API in Core PHP?

Building a REST API in Core PHP gives you full control over routing, authentication, database operations, and performance without relying on a framework. It's also an excellent way to understand how REST APIs work internally.
+

What data format is commonly used in PHP REST APIs?

JSON (JavaScript Object Notation) is the most widely used data format because it's lightweight, human-readable, and supported by virtually all programming languages and platforms.
+

How do I receive JSON data in PHP?

You can read incoming JSON from the request body and decode it into a PHP array or object before processing it.
+

How do I return JSON responses from a PHP API?

Set the appropriate Content-Type header and return structured JSON containing the response data, status, and any relevant messages or errors.
+

How should errors be handled in a REST API?

A well-designed API should return appropriate HTTP status codes (such as 200, 201, 400, 401, 404, and 500) along with descriptive JSON error messages to help clients identify and resolve issues.
+

Can I connect a PHP REST API to a MySQL database?

Yes. PHP REST APIs commonly use MySQL or MariaDB to store and retrieve data. Database operations should use prepared statements or parameterized queries to help prevent SQL Injection attacks.
+

Why is CORS important for REST APIs?

Cross-Origin Resource Sharing (CORS) allows web applications hosted on different domains to communicate with your API securely by controlling which origins are permitted to access it.
Previous Article

Streaming GPT-3.5-turbo Responses

Next Article

The Missing Piece in Most PHP REST API Tutorials: The Router

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨