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:
- JavaScript registers a callback (or a Promise handler) to be called when the response arrives.
- The request is handed off to the browser's networking layer, which runs on a separate OS thread.
- JavaScript continues executing other code.
- When the response arrives, the callback is placed on the event queue.
- 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
- User clicks link/button
- Browser sends HTTP request
- Server processes request
- Server sends complete HTML page
- Browser loads entire new page
- User triggers action
- JavaScript sends async request
- Server processes request
- Server sends data (JSON/XML)
- 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:
| Value | Constant | Meaning |
| 0 | UNSENT | Object created, open() not called yet |
| 1 | OPENED | open() called |
| 2 | HEADERS_RECEIVED | Server has sent response headers |
| 3 | LOADING | Response body is being downloaded |
| 4 | DONE | Request 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:
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.
- 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.
- 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
- 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'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.