AJAX PHP
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
- Each keystroke restarts a 300 ms timer (debouncing), so the server is asked only when the user pauses typing, not for every key.
URLSearchParamsbuilds a correctly encoded query string (username=Nila_K).- The PHP endpoint reads
$_GET['username'], validates it, and returns JSON. - 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
- The endpoint accepts only POST, because it changes data.
- 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="...">. - The JSON body is read from
php://inputand decoded with a depth limit andJSON_THROW_ON_ERROR. - The input is validated like any form input; validation errors are returned with status 422 and a field-specific structure the JavaScript can display.
- 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 Sends | PHP 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://inputwith 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