Session 5.8 – PHP AJAX Integration

Module 5: Database Connectivity and Web Services | Duration: 1 hr

Learning Objectives

By the end of this session, students will be able to:

  • Understand AJAX and asynchronous communication
  • Implement AJAX requests using XMLHttpRequest and Fetch API
  • Create PHP backend endpoints for AJAX requests
  • Handle JSON data exchange between client and server
  • Build dynamic, interactive web applications
  • Implement real-time features like live search and auto-complete

Introduction to AJAX

AJAX (Asynchronous JavaScript and XML) allows web pages to update asynchronously by exchanging data with a server behind the scenes. This means parts of a web page can be updated without reloading the entire page.

Key Insight

Despite the "X" in AJAX standing for XML, modern AJAX exchanges almost exclusively use JSON rather than XML because JSON is more compact, easier to parse in JavaScript, and maps directly to JavaScript objects. The term "AJAX" has become a synonym for any asynchronous client–server communication, regardless of data format.

The technique was popularised by Google Maps and Gmail around 2005, which demonstrated that web applications could feel as responsive as desktop software without ever fully reloading the page.

The Event Loop and Asynchrony

JavaScript runs on a single thread. If a network request were synchronous (blocking), the browser would freeze while waiting for the server — the user could not scroll, click, or type. AJAX avoids this by being asynchronous:

  1. JavaScript registers a callback (or a Promise handler) to be called when the response arrives.
  2. The request is handed off to the browser's networking layer, which runs on a separate OS thread.
  3. JavaScript continues executing other code.
  4. When the response arrives, the callback is placed on the event queue.
  5. Once the current call stack is empty, the JavaScript engine picks up and executes the callback.
Synchronous AJAX Is Deprecated

Both XMLHttpRequest and Fetch support synchronous calls (xhr.open('GET', url, false)). Synchronous AJAX blocks the browser UI, freezing scroll and interaction. Modern browsers display a console warning for synchronous XHR on the main thread, and the feature will eventually be removed. Always use the asynchronous form.

Traditional vs AJAX Request
Traditional Request
  1. User clicks link/button
  2. Browser sends HTTP request
  3. Server processes request
  4. Server sends complete HTML page
  5. Browser loads entire new page
AJAX Request
  1. User triggers action
  2. JavaScript sends async request
  3. Server processes request
  4. Server sends data (JSON/XML)
  5. JavaScript updates page content
Benefits of AJAX
  • Better User Experience: No page reloads, faster interactions
  • Reduced Bandwidth: Only data is transferred, not entire pages
  • Improved Performance: Asynchronous requests don't block the UI
  • Real-time Updates: Content can be updated dynamically
  • Modern Web Apps: Enables SPA (Single Page Application) architecture

XMLHttpRequest API

History and Role of XMLHttpRequest

XMLHttpRequest (XHR) was introduced by Microsoft as an ActiveX object in Internet Explorer 5 (1999) and later standardised by the WHATWG. For over a decade it was the only built-in way to make asynchronous HTTP requests from JavaScript.

XHR is event-driven: you assign handlers to properties like onload, onerror, and onprogress, then call send(). The browser calls your handlers when the corresponding events occur.

XHR Ready States

The readyState property tracks the lifecycle of an XHR request:

ValueConstantMeaning
0UNSENTObject created, open() not called yet
1OPENEDopen() called
2HEADERS_RECEIVEDServer has sent response headers
3LOADINGResponse body is being downloaded
4DONERequest complete (success or failure)

Use onload (fires when state reaches 4 and HTTP status is in the 2xx range) rather than the more complex onreadystatechange unless you specifically need to handle mid-request states.

Basic GET Request
// Create XMLHttpRequest object const xhr = new XMLHttpRequest(); // Configure request xhr.open('GET', 'api/users.php', true); // Set up callback for when request completes xhr.onload = function() { if (xhr.status === 200) { // Success console.log(xhr.responseText); document.getElementById('result').innerHTML = xhr.responseText; } else { // Error console.error('Request failed: ' + xhr.status); } }; // Handle network errors xhr.onerror = function() { console.error('Network error'); }; // Send request xhr.send();
POST Request with Data
const xhr = new XMLHttpRequest(); xhr.open('POST', 'api/create_user.php', true); // Set request header for form data xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function() { if (xhr.status === 200) { console.log('Success:', xhr.responseText); } }; // Send form data const data = 'name=John&email=john@example.com&age=30'; xhr.send(data); // Or send as FormData const formData = new FormData(); formData.append('name', 'John'); formData.append('email', 'john@example.com'); formData.append('age', 30); const xhr2 = new XMLHttpRequest(); xhr2.open('POST', 'api/create_user.php', true); xhr2.onload = function() { console.log(xhr2.responseText); }; xhr2.send(formData);
JSON Data Exchange
// Send JSON data const xhr = new XMLHttpRequest(); xhr.open('POST', 'api/users.php', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function() { if (xhr.status === 200) { // Parse JSON response const data = JSON.parse(xhr.responseText); console.log(data); // Use the data data.users.forEach(user => { console.log(user.name, user.email); }); } }; // Send JSON object const userData = { name: 'John Doe', email: 'john@example.com', age: 30 }; xhr.send(JSON.stringify(userData));
Progress and State Management
const xhr = new XMLHttpRequest(); // Track ready state changes xhr.onreadystatechange = function() { // 0 = UNSENT, 1 = OPENED, 2 = HEADERS_RECEIVED, // 3 = LOADING, 4 = DONE console.log('Ready state:', xhr.readyState); if (xhr.readyState === 4 && xhr.status === 200) { console.log('Complete:', xhr.responseText); } }; // Track upload progress xhr.upload.onprogress = function(event) { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log('Upload progress:', percentComplete + '%'); document.getElementById('progress').value = percentComplete; } }; // Track download progress xhr.onprogress = function(event) { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log('Download progress:', percentComplete + '%'); } }; xhr.open('POST', 'api/upload.php', true); xhr.send(formData);

Fetch API (Modern Approach)

Why Fetch Replaced XHR

The Fetch API, introduced in 2015 and now supported by all modern browsers, addresses several design problems with XHR:

  • Promise-based — Returns a Promise instead of using callbacks, enabling cleaner chaining with .then() and especially async/await.
  • Cleaner API — One function, fetch(url, options), replaces the multi-step open()/setRequestHeader()/send() sequence.
  • Streaming support — Can read response bodies as streams for incremental processing.
  • Service Worker integration — Fetch requests can be intercepted and handled by Service Workers for offline-first applications.
Fetch Does NOT Reject on HTTP Errors

fetch() only rejects its Promise when a network failure occurs (e.g. DNS not found, connection refused). A 404 or 500 response from the server is considered a successful fetch — the Promise resolves. Always check response.ok (true for status 200–299) or response.status explicitly.

Promises and async/await

A Promise is a JavaScript object representing a value that will be available in the future. It can be in one of three states: pending, fulfilled, or rejected. The async/await syntax is syntactic sugar over Promises that makes asynchronous code look synchronous:

const data = await fetch(url).then(r => r.json());

This pauses execution of the async function at await until the Promise resolves, then assigns the result — without blocking the browser thread.

Basic GET Request
// Simple GET request fetch('api/users.php') .then(response => response.json()) .then(data => { console.log(data); displayUsers(data.users); }) .catch(error => { console.error('Error:', error); }); // With async/await async function getUsers() { try { const response = await fetch('api/users.php'); const data = await response.json(); console.log(data); return data; } catch (error) { console.error('Error:', error); } } // Check response status fetch('api/users.php') .then(response => { if (!response.ok) { throw new Error('HTTP error ' + response.status); } return response.json(); }) .then(data => console.log(data)) .catch(error => console.error(error));
POST Request with JSON
// POST JSON data const userData = { name: 'John Doe', email: 'john@example.com', age: 30 }; fetch('api/create_user.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }) .then(response => response.json()) .then(data => { console.log('Success:', data); alert('User created!'); }) .catch(error => { console.error('Error:', error); }); // With async/await async function createUser(userData) { try { const response = await fetch('api/create_user.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await response.json(); if (data.success) { alert('User created successfully!'); } else { alert('Error: ' + data.message); } } catch (error) { console.error('Error:', error); alert('Network error occurred'); } }
FormData and File Upload
// Upload form data const form = document.getElementById('userForm'); const formData = new FormData(form); fetch('api/submit.php', { method: 'POST', body: formData // Don't set Content-Type header for FormData }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error)); // Upload file with progress async function uploadFile(file) { const formData = new FormData(); formData.append('file', file); try { const response = await fetch('api/upload.php', { method: 'POST', body: formData }); const data = await response.json(); console.log('Upload successful:', data); } catch (error) { console.error('Upload failed:', error); } } // Usage document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { uploadFile(file); } });

PHP Backend for AJAX

PHP as a JSON API

When PHP responds to an AJAX request it acts as a lightweight API endpoint. Three conventions distinguish an API response from a regular HTML page:

  1. header('Content-Type: application/json') — Tells the browser and any intermediary caches that the body is JSON. Without this, some browsers misinterpret the response type.
  2. JSON body only — The response must be pure JSON. Any stray whitespace, PHP notices, or HTML error messages before or after the JSON body will corrupt it and cause a parse error in JavaScript.
  3. Correct HTTP status codes — Use http_response_code(400) for bad input, 404 for missing resources, 500 for server errors. This lets the client distinguish error types without parsing the body.
Standard JSON Response Structure

Use a consistent response envelope across all endpoints so client code can handle responses uniformly:

// Success
{ "success": true, "data": { ... } }

// Error
{ "success": false, "message": "Human-readable error description" }
Turn Off PHP Error Display in Production

If display_errors = On in php.ini, a PHP fatal error inserts an HTML error page before your JSON output. The combined output is not valid JSON and will cause a parse error on the client. In production, set display_errors = Off and log_errors = On so errors go to the server log, not the response body.

Simple API Endpoint
<?php // api/users.php header('Content-Type: application/json'); try { $pdo = new PDO("mysql:host=localhost;dbname=mydb", "root", ""); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->query("SELECT id, name, email FROM users"); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode([ 'success' => true, 'users' => $users ]); } catch (PDOException $e) { http_response_code(500); echo json_encode([ 'success' => false, 'message' => 'Database error' ]); } ?>
Create User Endpoint
<?php // api/create_user.php header('Content-Type: application/json'); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Method not allowed']); exit; } // Get JSON input $input = json_decode(file_get_contents('php://input'), true); // Validate input if (empty($input['name']) || empty($input['email'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Name and email required']); exit; } try { $pdo = new PDO("mysql:host=localhost;dbname=mydb", "root", ""); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)"); $stmt->execute([ $input['name'], $input['email'], $input['age'] ?? null ]); echo json_encode([ 'success' => true, 'message' => 'User created successfully', 'user_id' => $pdo->lastInsertId() ]); } catch (PDOException $e) { http_response_code(500); echo json_encode([ 'success' => false, 'message' => 'Error creating user' ]); } ?>
Update and Delete Endpoints
<?php // api/update_user.php header('Content-Type: application/json'); $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? 0; try { $pdo = new PDO("mysql:host=localhost;dbname=mydb", "root", ""); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->prepare("UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?"); $stmt->execute([ $input['name'], $input['email'], $input['age'], $id ]); echo json_encode([ 'success' => true, 'message' => 'User updated successfully' ]); } catch (PDOException $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error updating user']); } // api/delete_user.php header('Content-Type: application/json'); $id = $_GET['id'] ?? 0; try { $pdo = new PDO("mysql:host=localhost;dbname=mydb", "root", ""); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$id]); echo json_encode([ 'success' => true, 'message' => 'User deleted successfully' ]); } catch (PDOException $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Error deleting user']); } ?>

Practical Examples

Debouncing — Throttling Input Events

A live-search input fires an input event on every keystroke. For a user typing "javascript" at normal speed, that is approximately 10 events in under 2 seconds. Making an AJAX call on every keystroke would send 10 requests to the server, most of which are for incomplete, intermediate queries.

Debouncing solves this by delaying execution of a function until a specified time has passed since the last call. A 300 ms debounce means the search only fires once the user has stopped typing for 300 ms — typically just one or two requests for the full query.

Throttling is related but different: it limits the function to run at most once per time interval regardless of how many calls are made. Throttling is better for scroll/resize events; debouncing is better for search inputs.

Live Search
<!-- HTML --> <input type="text" id="searchInput" placeholder="Search users..."> <div id="searchResults"></div> <script> const searchInput = document.getElementById('searchInput'); const searchResults = document.getElementById('searchResults'); // Debounce function to limit API calls function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Search function async function searchUsers(query) { if (query.length < 2) { searchResults.innerHTML = ''; return; } try { const response = await fetch(`api/search.php?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.success) { displayResults(data.users); } } catch (error) { console.error('Search error:', error); } } // Display results function displayResults(users) { if (users.length === 0) { searchResults.innerHTML = '<p>No results found</p>'; return; } const html = users.map(user => ` <div class="result-item"> <strong>${user.name}</strong> - ${user.email} </div> `).join(''); searchResults.innerHTML = html; } // Add event listener with debounce searchInput.addEventListener('input', debounce((e) => { searchUsers(e.target.value); }, 300)); </script> <?php // api/search.php header('Content-Type: application/json'); $query = $_GET['q'] ?? ''; try { $pdo = new PDO("mysql:host=localhost;dbname=mydb", "root", ""); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE name LIKE ? OR email LIKE ? LIMIT 10"); $searchTerm = "%$query%"; $stmt->execute([$searchTerm, $searchTerm]); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'users' => $users]); } catch (PDOException $e) { echo json_encode(['success' => false, 'message' => 'Search failed']); } ?>
Dynamic Form Submission
<!-- HTML --> <form id="userForm"> <input type="text" name="name" placeholder="Name" required> <input type="email" name="email" placeholder="Email" required> <input type="number" name="age" placeholder="Age"> <button type="submit">Create User</button> </form> <div id="message"></div> <div id="usersList"></div> <script> const form = document.getElementById('userForm'); const messageDiv = document.getElementById('message'); const usersList = document.getElementById('usersList'); // Handle form submission form.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(form); const userData = Object.fromEntries(formData); try { const response = await fetch('api/create_user.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await response.json(); if (data.success) { messageDiv.innerHTML = '<div class="alert alert-success">User created!</div>'; form.reset(); loadUsers(); // Refresh user list } else { messageDiv.innerHTML = `<div class="alert alert-danger">${data.message}</div>`; } } catch (error) { messageDiv.innerHTML = '<div class="alert alert-danger">Network error</div>'; } }); // Load users async function loadUsers() { try { const response = await fetch('api/users.php'); const data = await response.json(); if (data.success) { displayUsers(data.users); } } catch (error) { console.error('Error loading users:', error); } } // Display users function displayUsers(users) { const html = users.map(user => ` <div class="user-item"> <span>${user.name} - ${user.email}</span> <button onclick="deleteUser(${user.id})">Delete</button> </div> `).join(''); usersList.innerHTML = html; } // Delete user async function deleteUser(id) { if (!confirm('Delete this user?')) return; try { const response = await fetch(`api/delete_user.php?id=${id}`); const data = await response.json(); if (data.success) { loadUsers(); // Refresh list } else { alert('Error deleting user'); } } catch (error) { alert('Network error'); } } // Load users on page load loadUsers(); </script>
Auto-refresh Content
<!-- Real-time notifications --> <div id="notifications"></div> <script> async function checkNotifications() { try { const response = await fetch('api/notifications.php'); const data = await response.json(); if (data.success && data.notifications.length > 0) { displayNotifications(data.notifications); } } catch (error) { console.error('Error fetching notifications:', error); } } function displayNotifications(notifications) { const container = document.getElementById('notifications'); notifications.forEach(notification => { const div = document.createElement('div'); div.className = 'alert alert-info'; div.textContent = notification.message; container.appendChild(div); // Remove after 5 seconds setTimeout(() => div.remove(), 5000); }); } // Check every 10 seconds setInterval(checkNotifications, 10000); // Check immediately checkNotifications(); </script>

Best Practices

CSRF — Cross-Site Request Forgery

A CSRF attack tricks an authenticated user's browser into making a state-changing request (e.g. delete an account) to your server from a different website. The attacker's page might contain an invisible <img src="https://yoursite.com/delete_account.php"> that the browser fetches with the user's session cookie.

Standard protection: include a unique, server-generated CSRF token in every form or AJAX request. The PHP handler verifies the token before processing. Tokens are unforgeable because the attacker's site cannot read your site's cookies (Same-Origin Policy).

CORS — Cross-Origin Resource Sharing

Browsers enforce the Same-Origin Policy: a script at https://frontend.example.com cannot make AJAX calls to https://api.example.com unless the server explicitly allows it. CORS is the mechanism that lets servers declare which origins are permitted, using response headers:

  • Access-Control-Allow-Origin: https://frontend.example.com — Allow a specific origin.
  • Access-Control-Allow-Origin: * — Allow any origin (only for truly public APIs).
  • Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  • Access-Control-Allow-Headers: Content-Type, Authorization
Do's
  • Use Fetch API for modern browsers
  • Always handle errors properly
  • Validate input on both client and server
  • Use appropriate HTTP methods
  • Set correct Content-Type headers
  • Implement request throttling/debouncing
  • Show loading indicators
  • Return consistent JSON responses
  • Use CORS headers when needed
  • Implement proper security measures
Don'ts
  • Don't make excessive AJAX requests
  • Don't ignore error responses
  • Don't trust client-side validation only
  • Don't expose sensitive data in responses
  • Don't use synchronous AJAX calls
  • Don't forget to sanitize user input
  • Don't return HTML from API endpoints
  • Don't block UI during requests
  • Don't hardcode API endpoints
  • Don't forget about backwards compatibility
Security Considerations
  • CSRF Protection: Implement CSRF tokens for state-changing operations
  • Authentication: Verify user authentication for protected endpoints
  • Input Validation: Validate and sanitize all input on the server
  • Rate Limiting: Prevent abuse with request rate limiting
  • HTTPS: Always use HTTPS for sensitive data
  • CORS: Configure CORS properly to restrict access

Session Summary

Key Points
  • AJAX enables asynchronous communication between client and server
  • Fetch API is the modern, promise-based approach for AJAX requests
  • XMLHttpRequest is still widely supported for legacy browsers
  • PHP backend should return JSON responses with proper headers
  • Always handle errors and provide user feedback
  • Implement debouncing for frequent requests like live search
  • Validate input on both client and server sides
  • Use appropriate HTTP methods (GET, POST, PUT, DELETE)
  • AJAX enables real-time features and better user experience
Next Session Preview

In the next session, we will explore Introduction to ASP.NET, learning about Microsoft's web application framework and how it compares to PHP development.