AJAX PHP

JA JASWANT Updated 18 Oct 2026
6 min read ·Lesson 109 of 248
In this post

PHP Endpoints for AJAX Requests In simple terms: In an AJAX feature, JavaScript in the browser asks a PHP script a question — "Is this username available?", "Save this comment" — and the PHP script an

PHP Endpoints for AJAX Requests

In simple terms: In an AJAX feature, JavaScript in the browser asks a PHP script a question — "Is this username available?", "Save this comment" — and the PHP script answers with data, usually JSON. The PHP side is an endpoint: a script that reads the request, does its work, and responds with a status code and a small data structure instead of an HTML page.

Technically: PHP receives AJAX requests like any other HTTP request. Query parameters arrive in $_GET; form-encoded or FormData bodies arrive in $_POST; JSON bodies must be read from php://input and decoded. The endpoint validates the input, checks permissions, and returns JSON with an appropriate HTTP status code.


A Reusable JSON Response Helper

<?php
// src/json-response.php
function jsonResponse(int $status, array $data): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store');

    echo json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    exit;
}

Every endpoint in this section uses this helper. The never return type documents that it ends the script (see Mixed and Never Types).


GET Requests: Checking a Username

The Validation Logic

Keeping the logic in a function makes it testable without a browser:

<?php
function checkUsername(string $input, array $takenUsernames): array
{
    $username = strtolower(trim($input));

    if (!preg_match('/^[a-z0-9_]{3,20}$/', $username)) {
        return ['available' => false, 'message' => 'Use 3–20 letters, digits, or underscores.'];
    }

    if (in_array($username, $takenUsernames, true)) {
        return ['available' => false, 'message' => 'This username is already taken.'];
    }

    return ['available' => true, 'message' => 'Available!'];
}

$taken = ['asha', 'bala', 'admin'];

foreach (['ab', 'Asha', 'Nila_K', 'admin!'] as $candidate) {
    echo str_pad($candidate, 8) . json_encode(checkUsername($candidate, $taken), JSON_UNESCAPED_UNICODE) . "\n";
}

Output

ab      {"available":false,"message":"Use 3–20 letters, digits, or underscores."}
Asha    {"available":false,"message":"This username is already taken."}
Nila_K  {"available":true,"message":"Available!"}
admin!  {"available":false,"message":"Use 3–20 letters, digits, or underscores."}

The Endpoint: api/check-username.php

<?php
declare(strict_types=1);

require __DIR__ . '/../src/json-response.php';
require __DIR__ . '/../src/check-username.php';   // the function above
require __DIR__ . '/../src/pdo.php';

$input = $_GET['username'] ?? '';

if (!is_string($input)) {
    jsonResponse(400, ['error' => 'Invalid request.']);
}

$taken = pdo()->query('SELECT LOWER(username) FROM users')->fetchAll(PDO::FETCH_COLUMN);

jsonResponse(200, checkUsername($input, $taken));

(This assumes a users table with a username column. In a real application, query only the requested name — SELECT 1 FROM users WHERE username = ? — instead of loading all usernames. The list form is used here only to reuse the tested function directly.)

The JavaScript: Checking While Typing

<label for="username">Username</label>
<input id="username" name="username" autocomplete="username">
<p id="username-status" aria-live="polite"></p>

<script>
    const input = document.getElementById('username');
    const status = document.getElementById('username-status');
    let timer;

    input.addEventListener('input', () => {
        clearTimeout(timer);

        timer = setTimeout(async () => {
            const params = new URLSearchParams({ username: input.value });

            try {
                const response = await fetch(`api/check-username.php?${params}`);
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const result = await response.json();
                status.textContent = result.message;
                status.className = result.available ? 'ok' : 'error';
            } catch {
                status.textContent = 'Could not check the username right now.';
            }
        }, 300);
    });
</script>

How It Works

  1. Each keystroke restarts a 300 ms timer (debouncing), so the server is asked only when the user pauses typing, not for every key.
  2. URLSearchParams builds a correctly encoded query string (username=Nila_K).
  3. The PHP endpoint reads $_GET['username'], validates it, and returns JSON.
  4. The response message is displayed with textContent.

The availability check improves the user experience, but registration must still validate the username again on the server and rely on a UNIQUE index — another user could take the name in the meantime.


POST Requests With FormData

Sending a form's fields with FormData produces a normal form submission body, so PHP reads it from $_POST — exactly like a regular form:

const form = document.getElementById('comment-form');

form.addEventListener('submit', async (event) => {
    event.preventDefault();

    const response = await fetch(form.action, {
        method: 'POST',
        body: new FormData(form),   // includes the hidden CSRF token field
    });

    const result = await response.json();
    // show result.message or the validation errors
});

This approach is ideal for progressive enhancement: without JavaScript, the form submits normally to the same endpoint; with JavaScript, it is sent in the background. The PHP script can decide which response to give based on the Accept header (application/json vs HTML).


POST Requests With a JSON Body

APIs often receive JSON. PHP does not fill $_POST for JSON bodies; read and decode the raw input:

<?php
// api/subscribe.php
declare(strict_types=1);

require __DIR__ . '/../src/json-response.php';

session_start();

if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
    jsonResponse(405, ['error' => 'Use POST.']);
}

$sessionToken = $_SESSION['csrf_token'] ?? '';
$sentToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';

if ($sessionToken === '' || !is_string($sentToken) || !hash_equals($sessionToken, $sentToken)) {
    jsonResponse(419, ['error' => 'Session expired. Please reload the page.']);
}

try {
    $data = json_decode(file_get_contents('php://input'), true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    jsonResponse(400, ['error' => 'Invalid JSON.']);
}

$email = is_array($data) && is_string($data['email'] ?? null) ? trim($data['email']) : '';

if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    jsonResponse(422, ['errors' => ['email' => 'Please enter a valid email address.']]);
}

// ... save the subscription ...

jsonResponse(201, ['message' => 'Thanks for subscribing!']);
const response = await fetch('api/subscribe.php', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
    },
    body: JSON.stringify({ email: emailInput.value }),
});

const result = await response.json();

if (response.status === 201) {
    message.textContent = result.message;
} else if (response.status === 422) {
    message.textContent = result.errors.email;
} else {
    message.textContent = result.error ?? 'Something went wrong.';
}

Step-by-Step Explanation

  1. The endpoint accepts only POST, because it changes data.
  2. A CSRF token is sent in a custom header and compared with the session token using hash_equals(). The check fails if the session has no token at all — otherwise an empty header would match an empty session value. The token is placed in the page, for example as <meta name="csrf-token" content="...">.
  3. The JSON body is read from php://input and decoded with a depth limit and JSON_THROW_ON_ERROR.
  4. The input is validated like any form input; validation errors are returned with status 422 and a field-specific structure the JavaScript can display.
  5. Success returns status 201 Created.

Session cookies are sent automatically with same-origin fetch() requests, so $_SESSION works in AJAX endpoints just as in normal pages.


Where Request Data Arrives

JavaScript SendsPHP Reads
Query string (?q=lamp)$_GET['q']
FormData or application/x-www-form-urlencoded body$_POST, $_FILES
JSON body (application/json)json_decode(file_get_contents('php://input'), true)
Custom headers (X-CSRF-Token)$_SERVER['HTTP_X_CSRF_TOKEN']

Common Mistakes

Expecting JSON Bodies in $_POST

Effect: $_POST is empty.

Fix: Read and decode php://input.

Trusting Client-Side Checks

Effect: Invalid or malicious data reaches the database when requests are sent directly.

Fix: Validate everything on the server.

State-Changing GET Requests

Effect: Actions can be triggered by links, prefetching, or other sites.

Fix: Use POST with CSRF protection.

Returning 200 for Errors

Effect: JavaScript cannot distinguish failures from success.

Fix: Use appropriate status codes (400, 401, 403, 404, 422, 500).

Sending a Request on Every Keystroke

Effect: Unnecessary server load and out-of-order responses.

Fix: Debounce input events (and cancel outdated requests — see AJAX Live Search).


Best Practices

  • Write endpoints that validate input, check permissions, and return JSON with proper status codes.
  • Keep business logic in testable functions or classes; keep endpoints thin.
  • Use GET for reading data and POST with CSRF tokens for changes.
  • Read JSON bodies from php://input with error handling and depth limits.
  • Debounce frequent requests and handle errors gracefully in JavaScript.

Official reference: https://www.php.net/manual/en/reserved.variables.get.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about AJAX PHP and get answers drawn from it.

Signed-in readers only.