Talking to PHP from JavaScript: fetch, jQuery and JSON done properly
A browser page that asks a PHP script a question and gets an answer back without reloading is still one of the most useful patterns on the web. Here is the modern way to do it, and how it maps onto the jQuery code you may be maintaining.
The conversation in one picture
Every exchange has the same four steps: the page builds a request (URL, method, body), the browser sends it, PHP reads the input and replies with a status code and a body, and the page reads that reply. Most bugs come from the two sides disagreeing about the format of the body. The simplest rule to avoid them: send JSON, receive JSON, and say so in the Content-Type header both ways.
The PHP side
Here is a small endpoint, check.php, that receives a word and says whether it is in a list. It shows every piece you need: method check, input parsing, validation, a JSON reply and correct status codes.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
echo json_encode(['error' => 'Use POST']);
exit;
}
try {
$data = json_decode(file_get_contents('php://input'), true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(400);
echo json_encode(['error' => 'Body must be valid JSON']);
exit;
}
$word = is_array($data) && isset($data['word']) ? trim((string) $data['word']) : '';
if ($word === '' || mb_strlen($word) > 64) {
http_response_code(422);
echo json_encode(['error' => 'Send a word of 1 to 64 characters']);
exit;
}
$known = ['receive', 'separate', 'definitely'];
echo json_encode([
'word' => $word,
'known' => in_array(mb_strtolower($word), $known, true),
], JSON_UNESCAPED_UNICODE);
Three details trip people up:
$_POSTis empty for JSON. PHP only fills$_POSTfor form-encoded (application/x-www-form-urlencoded) andmultipart/form-databodies. A JSON body has to be read from the raw input stream,php://input, and decoded. The PHP manual's page onphp://wrappers describes the stream.json_decodefails quietly by default. Without flags it returnsnullon invalid input, which is indistinguishable from a body ofnull. TheJSON_THROW_ON_ERRORflag, available since PHP 7.3, makes it throw aJsonExceptioninstead (see json_decode).- Nothing may be printed before the header. A stray space before
<?php, a PHP notice or avar_dumpleft in the code will be sent first and corrupt the JSON. If the page's JavaScript reports "Unexpected token <", the server almost certainly sent an HTML error page instead of JSON.
The browser side with fetch
All current browsers include the Fetch API, so no library is needed:
async function checkWord(word) {
const res = await fetch('/check.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ word })
});
const data = await res.json().catch(() => null);
if (!res.ok) {
throw new Error(data && data.error ? data.error : 'HTTP ' + res.status);
}
return data; // { word: "...", known: true|false }
}
checkWord('recieve')
.then(r => console.log(r.known ? 'known' : 'not in the list'))
.catch(err => console.error(err.message));
The detail people miss most often: fetch does not reject on HTTP errors. A 404 or 500 response still resolves; only network failures reject. Always check res.ok (true for status 200–299) before using the data, as above.
The same thing with jQuery
A great deal of existing code, especially in older PHP projects, uses jQuery's $.ajax. The equivalent call is:
$.ajax({
url: '/check.php',
method: 'POST',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({ word: 'recieve' })
})
.done(function (r) { console.log(r.known); })
.fail(function (xhr) {
var msg = xhr.responseJSON && xhr.responseJSON.error;
console.error(msg || 'HTTP ' + xhr.status);
});
Two differences from fetch are worth knowing. jQuery's .fail does run for HTTP error statuses. And if you pass a plain object as data without contentType, jQuery sends it form-encoded, in which case PHP fills $_POST and php://input holds a query string, not JSON. Mixing the two conventions is the classic source of "my PHP receives nothing". Pick one: either send form data and read $_POST, or send JSON and read php://input. The jQuery.ajax documentation lists every option.
If you are writing new code, fetch is usually the better choice: it is built in, it returns promises, and it works with async/await. There is no need to rewrite working jQuery code just for that, though.
Sending a form
For an ordinary HTML form, including file uploads, you do not need JSON at all. Pass the form element to FormData and PHP sees the fields in $_POST and the files in $_FILES:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const res = await fetch(form.action, { method: 'POST', body: new FormData(form) });
// do not set Content-Type yourself: the browser adds the multipart boundary
});
Security checklist
- Validate on the server. JavaScript checks are a convenience for the user; anyone can call your endpoint directly with any body.
- Escape on output. When the page inserts a reply into the document, use
textContentrather thaninnerHTML, or escape the text. On the PHP side,htmlspecialchars()is for HTML output; for JSON replies,json_encodealready produces valid, safely quoted strings. - Protect state-changing requests. If the endpoint changes data and relies on a session cookie, defend against cross-site request forgery with a token or with
SameSitecookies. The OWASP CSRF prevention cheat sheet explains the options. - Use prepared statements (PDO or mysqli) if the input reaches a database. Never build SQL by concatenating request data.
- Return real status codes. 400 for malformed input, 401 or 403 for authentication problems, 422 for valid JSON with invalid values, 500 for server faults. Clients and monitoring tools rely on them.
Debugging
Open the browser's developer tools, go to the Network panel and click the request. The Headers tab shows the method, URL, status and the Content-Type of both request and response; the Payload tab shows what you actually sent; the Response tab shows what PHP actually returned. Nine times out of ten the problem is visible there in seconds: a 404 from a wrong path, an HTML warning before the JSON, or a form-encoded body when PHP was expecting JSON.