Master how client-side user interfaces communicate with backend REST APIs and databases. Learn the Fetch API, asynchronous execution, and JSON data flow. Implement production-grade Loading, Success, and Error states, operate an interactive 3-pane integration playground, and keep UI state synchronized with backend persistence.
In modern full-stack engineering, API Integration is the process of connecting a frontend client application (browser or mobile UI) to a backend server and database over HTTP.
A browser cannot directly connect to a database like PostgreSQL or MongoDB. Doing so would expose secret database credentials and allow any user to run destructive SQL queries. The API acts as a secure, authenticated gateway that validates requests before touching the database.
Network requests take time (e.g. 50ms–500ms). JavaScript is single-threaded; if API calls were synchronous, the browser screen would freeze completely until the server responded. The Fetch API uses Promises and async/await to execute requests in the background while keeping the UI responsive.
1. OUTGOING REQUEST: JavaScript Object -> JSON.stringify(object) -> Raw JSON String -> HTTP POST Body 2. INCOMING RESPONSE: HTTP Response Stream -> await response.json() -> Deserialized JavaScript Object/Array
The browser standard fetch() method accepts a URL and an optional configuration object specifying the HTTP method, headers, and body:
async function getTasks() {
const response = await fetch('/api/tasks');
// Always check response.ok!
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const tasks = await response.json();
return tasks;
}async function addTask(title) {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, completed: false })
});
if (!response.ok) {
throw new Error('Failed to create task');
}
return await response.json();
}async function toggleTask(id, completed) {
const response = await fetch(`/api/tasks/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ completed })
});
if (!response.ok) throw new Error('Update failed');
return await response.json();
}async function deleteTask(id) {
const response = await fetch(`/api/tasks/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Delete failed');
// 204 No Content has no body, so do NOT call .json()
return true;
}According to the WHATWG Fetch Standard, fetch() only rejects a Promise on network-level failures (e.g. loss of internet connection, DNS failure, or CORS rejection). It does not reject when the server returns 404 Not Found or 500 Internal Server Error! You must always check if (!response.ok) to guard against HTTP error status codes.
Experience real-time frontend ↔ backend communication. Edit the frontend integration code on the left, observe the live application UI on the right, and inspect every network packet in the real-time inspector below:
Every production web application must manage the three fundamental states of an asynchronous API operation. Click each scenario to observe how the UI component dynamically adapts:
Network request in flight. Displays spinners, skeleton loaders, or disables submit buttons.
HTTP 200 OK received. Data is parsed and rendered onto the screen.
Backend threw an exception. Displays error banner with retry button.
User went offline or DNS failed. fetch() rejected with TypeError.
See how full-stack CRUD operations (GET, POST, PATCH, DELETE) maintain strict synchronization between user interface state and backend database records:
Diagnose real integration bugs encountered by web developers when connecting frontend code to backend APIs:
async function getTasks() {
const response = await fetch('/api/tasks');
const tasks = response.json(); // <-- Missing await!
console.log(tasks);
return tasks;
}Why did tasks evaluate to a Promise instead of the actual data array?
fetch('/api/tasks', {
method: 'POST',
body: JSON.stringify({ title: 'Configure Nginx' })
// Missing headers: { 'Content-Type': 'application/json' }
});Why did the backend fail to read the task title?
async function loadUser(userId) {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
// Forgot: if (!res.ok) throw new Error(...)
setUser(data);
}Why didn't fetch() automatically throw an error on the 404 response?
async function fetchProducts() {
setLoading(true);
const res = await fetch('/api/products'); // User lost internet!
const data = await res.json();
setProducts(data);
setLoading(false);
}How should this function be fixed so the loading spinner stops and an error message appears?
Connect a modern frontend application to a backend Product Dashboard API (/api/products). Complete each architectural implementation step:
You are mounting the Product Dashboard. You need to fetch all products from GET /api/products, display a loading indicator while the network request is pending, and handle non-2xx errors.
async function loadProducts() {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/products');
// What code belongs here?
const data = await res.json();
setProducts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}