Talking to PHP from JavaScript: fetch, jQuery and JSON done properly

Under the hood · Published 2026-09-17

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:

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

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.