Topic 25 of 56 · Full Stack Advanced

Topic 10 : REST APIs

Lesson TL;DRTopic 10: REST APIs 📖 10 min read · 🎯 advanced · 🧭 Prerequisites: introductiontoredux, librariesimagepickerreactelements Why this matters Up until now, your React app has been living in its own lit...
10 min read·advanced·rest-api · fetch-api · axios · react-hooks

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:

  1. Fetching Data — Using the Fetch API or Axios to retrieve JSON from a remote server via HTTP.
  2. Managing State — Using React's useState hook to store the fetched data, a loading flag, and any error that surfaces.
  3. Handling Side Effects — Using useEffect to 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.ok must 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 .then branches and the .catch branch call setLoading(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, not response.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:

  • newUser is 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 users array 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.ok check; Axios auto-throws on non-2xx and auto-parses JSON into response.data.
  • Always pair data fetching with three state variables: the data array, a loading boolean, and an error object.
  • useEffect with 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

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.