Topic 10: REST APIs
📖 10 min read · 🎯 advanced · 🧭 Prerequisites: introduction-to-redux, libraries-image-picker-react-elements
Why this matters
Up until now, your React app has been living in its own little bubble — data you typed in, arrays you wrote by hand, state you made up yourself. But real apps don't work that way. Real apps talk to servers. They fetch user profiles, submit order forms, pull product lists — all by sending requests over the internet and handling what comes back. That's exactly what REST APIs are for. In this lesson, you'll learn two ways to do it — the Fetch API and Axios — and how to keep those calls clean and out of your components.
What You'll Learn
- Fetch data from a REST API using the native Fetch API inside a React component
- Replace Fetch with Axios and understand the differences in response handling
- Extract API calls into a dedicated service file for separation of concerns
- Handle GET and POST requests together with proper loading, error, and success states
The Analogy
Think of your React component as a restaurant dining room — it sets the table, displays the food, and takes new orders. The kitchen is the REST API living on a remote server. Your component should never walk into the kitchen itself; instead, it hands a ticket to a waiter. That waiter is your API service layer. The waiter knows the kitchen's menu (endpoints), speaks its language (HTTP), and brings back exactly what was ordered (JSON). Keeping the dining room and kitchen separated means you can swap kitchens entirely — say, migrate from jsonplaceholder to your own backend — without rearranging a single table.
Chapter 1: Fetching Data in React — Core Concepts
Before writing a single line of code, the trainer outlined the three pillars every data-fetching flow rests on:
- Fetching Data — Using the Fetch API or Axios to retrieve JSON from a remote server via HTTP.
- Managing State — Using React's
useStatehook to store the fetched data, a loading flag, and any error that surfaces. - Handling Side Effects — Using
useEffectto trigger the fetch after the component mounts, keeping the render function pure.
Every example in this lesson follows this same three-pillar pattern. Recognising it in any codebase is the first skill; applying it consistently is the second.
sequenceDiagram
participant Component
participant useEffect
participant API_Service
participant Remote_Server
Component->>useEffect: mounts ([] dependency)
useEffect->>API_Service: call fetch / axios
API_Service->>Remote_Server: HTTP GET /users
Remote_Server-->>API_Service: 200 OK + JSON array
API_Service-->>useEffect: resolved Promise
useEffect->>Component: setUsers(data), setLoading(false)
Component->>Component: re-render with user list
Chapter 2: Fetching Data with the Fetch API
Step 1 — Create a New React Project
npx create-react-app fetch-api-demo
cd fetch-api-demo
Step 2 — Build the Users Component
Create src/components/Users.js. This component owns three pieces of state — the data array, a loading boolean, and an error object — and kicks off the fetch inside useEffect.
import React, { useState, useEffect } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default Users;
Key details worth noting:
response.okmust be checked manually with Fetch — a 404 or 500 does not throw automatically.- The empty dependency array
[]means the effect runs once, on mount. - Both
.thenbranches and the.catchbranch callsetLoading(false)so the spinner always resolves.
Step 3 — Integrate into App
import React from 'react';
import './App.css';
import Users from './components/Users';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>React Fetch API Demo</h1>
</header>
<Users />
</div>
);
}
export default App;
Step 4 — Run the Application
npm start
Navigating to http://localhost:3000 displays the list of users fetched from jsonplaceholder.typicode.com.
Chapter 3: Fetching Data with Axios
Axios is a popular HTTP client that wraps XMLHttpRequest (and Node's http module) in a clean Promise-based API. Unlike Fetch, Axios:
- Automatically parses JSON — the payload lands on
response.data, notresponse.json(). - Throws on non-2xx status codes — no need to check
response.ok. - Supports request/response interceptors, timeout config, and cancellation tokens out of the box.
Step 1 — Install Axios
npm install axios
Step 2 — Update the Users Component to Use Axios
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
setUsers(response.data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default Users;
Notice how the .then handler is now a single line — setUsers(response.data) — because Axios already parsed the JSON body into response.data. The component logic is otherwise identical.
Chapter 4: Handling API Requests in a Separate Service
Embedding axios.get(...) directly inside a component creates two problems: the URL is scattered across the codebase, and the component becomes hard to test in isolation. The fix is a dedicated service file.
Step 1 — Create the API Service
import axios from 'axios';
const API_URL = 'https://jsonplaceholder.typicode.com';
export const getUsers = () => {
return axios.get(`${API_URL}/users`);
};
Save this as src/services/userService.js. The API_URL constant lives in one place; swapping to a production URL is a single-line change.
Step 2 — Consume the Service in the Component
import React, { useState, useEffect } from 'react';
import { getUsers } from '../services/userService';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
getUsers()
.then((response) => {
setUsers(response.data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default Users;
The component no longer knows what URL it is hitting, which library it is using, or even whether the data comes from a network call or a cache. That is good separation of concerns.
Chapter 5: Advanced API Handling — POST Requests and Form State
Real applications don't just read data — they create it. the trainer walked the class through adding a createUser function to the service and wiring it up to a form.
Expand the Service with createUser
import axios from 'axios';
const API_URL = 'https://jsonplaceholder.typicode.com';
export const getUsers = () => {
return axios.get(`${API_URL}/users`);
};
export const createUser = (user) => {
return axios.post(`${API_URL}/users`, user);
};
axios.post takes the URL as the first argument and the request body as the second. Axios serialises the object to JSON and sets Content-Type: application/json automatically.
Add a Form to the Users Component
import React, { useState, useEffect } from 'react';
import { getUsers, createUser } from '../services/userService';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newUser, setNewUser] = useState({ name: '', email: '' });
useEffect(() => {
getUsers()
.then((response) => {
setUsers(response.data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, []);
const handleSubmit = (e) => {
e.preventDefault();
createUser(newUser)
.then((response) => {
setUsers([...users, response.data]);
setNewUser({ name: '', email: '' });
})
.catch((error) => {
setError(error);
});
};
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
<h2>Create New User</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
placeholder="Name"
/>
<input
type="email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
placeholder="Email"
/>
<button type="submit">Create User</button>
</form>
</div>
);
}
export default Users;
State management highlights:
newUseris a separate state object that tracks form fields; it is reset to{ name: '', email: '' }after a successful POST.- On success, the response body (the new user object returned by the API) is appended to the
usersarray with the spread pattern[...users, response.data], keeping the list reactive without a second GET. e.preventDefault()stops the browser from doing a full-page form submission.
🧪 Try It Yourself
Task: Build a Posts component that fetches from https://jsonplaceholder.typicode.com/posts and lets the user create a new post by submitting a title and body via a form.
Success criteria:
- On load, you see a list of post titles rendered on the page.
- After submitting the form, the new post title appears at the bottom of the list without refreshing the page.
- The form fields clear after a successful submit.
Starter — src/services/postService.js:
import axios from 'axios';
const API_URL = 'https://jsonplaceholder.typicode.com';
export const getPosts = () => axios.get(`${API_URL}/posts`);
export const createPost = (post) => axios.post(`${API_URL}/posts`, post);
Wire up getPosts and createPost in a Posts component following the same pattern as Users.
🔍 Checkpoint Quiz
Q1. When using the native Fetch API, why must you manually check response.ok before calling response.json()?
A) Fetch only works with HTTPS endpoints
B) Fetch does not throw on HTTP error status codes like 404 or 500
C) response.json() requires authentication headers to be verified first
D) React's useEffect catches network errors before they reach .catch
Q2. Given this code, what is logged if the server returns a 404?
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/nonexistent')
.then((res) => console.log('data:', res.data))
.catch((err) => console.log('error:', err.message));
}, []);
A) data: undefined
B) error: Request failed with status code 404
C) Nothing — Axios silently swallows 404s
D) The component crashes without reaching .catch
Q3. What is the bug in this handleSubmit handler?
const handleSubmit = (e) => {
createUser(newUser)
.then((response) => {
setUsers([...users, response.data]);
setNewUser({ name: '', email: '' });
});
};
A) createUser should receive a JSON string, not an object
B) e.preventDefault() is missing, so the browser reloads the page
C) response.data should be response.json()
D) The spread [...users, response.data] creates an infinite loop
Q4. You are building a React app that hits three different REST endpoints: /users, /posts, and /comments. How would you organise your service layer for maximum maintainability?
A) Put all three axios.get calls directly inside their respective components
B) Create a single apiService.js exporting all functions, using one shared API_URL constant
C) Create separate service files (userService.js, postService.js, commentService.js), each importing Axios and referencing a shared base URL
D) Use a global variable on window to store the Axios instance so all components can access it
A1. B — Fetch resolves (rather than rejects) the Promise for any completed HTTP response, including 4xx and 5xx codes. You must inspect response.ok yourself and throw if it is false.
A2. B — Axios treats any non-2xx status as an error and rejects the Promise, so .catch is called with a descriptive error message including the status code.
A3. B — Without e.preventDefault(), submitting the form triggers the browser's default behavior (a full page reload), which clears all React state and cancels any in-flight requests.
A4. C — Separate service files per resource keeps each file focused and small while a shared API_URL constant (perhaps extracted to an api.js config module) means you update the base URL in one place. Option B works for tiny apps but grows unwieldy. Option A scatters URLs across components. Option D is a global-state anti-pattern.
🪞 Recap
- The Fetch API requires a manual
response.okcheck; Axios auto-throws on non-2xx and auto-parses JSON intoresponse.data. - Always pair data fetching with three state variables: the data array, a
loadingboolean, and anerrorobject. useEffectwith an empty dependency array[]triggers the fetch exactly once, after the component mounts.- Isolating API calls in a service file (e.g.,
userService.js) decouples component logic from URL structure and HTTP library choice. - POST requests return the newly created resource in
response.data; append it to local state with the spread pattern to avoid a redundant GET.
📚 Further Reading
- JSONPlaceholder API docs — the free fake REST API used in every example above
- Axios GitHub README — full configuration options, interceptors, and cancellation tokens
- MDN Fetch API — the source of truth on the native Fetch API, including
Request/Responseobject details - React
useEffectdocs — official reference for side-effect timing and dependency arrays - ⬅️ Previous: Libraries Image Picker React Elements
- ➡️ Next: Actions and Reducers