Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 76

External API Integration with Error Handling

Recommended resources

Hello! In our last lesson, we built a fully functional form using controlled components. You learned how to capture user input in state and handle form submissions. However, any data we add is lost as soon as we refresh the page.

Today, we'll bridge that final gap by connecting our React application to an external API. This will allow our application to persist data, making it dynamic and truly interactive. We'll focus on fetching existing data to display when a component loads and sending new data from our form to be saved on a server. Crucially, we'll also implement loading and error states to ensure a smooth and informative user experience.

By the end of this lesson, you will have integrated a backend API into your React application, complete with robust loading and error handling for both fetching and submitting data.

Setting Up a Mock API with json-server

To practice working with an API without needing a complex backend, we'll use a fantastic tool called json-server. It lets you spin up a full fake REST API from a simple JSON file in seconds.

First, create a file named db.json in the root of your project. This file will act as our database. Let's add some initial data to it, for example, a list of jobs:

db.json

{
  "jobs": [
    {
      "id": "1",
      "title": "Senior React Developer",
      "type": "Full-Time",
      "description": "We are seeking a Senior React Developer to join our team...",
      "location": "Boston, MA",
      "salary": "$120,000 - $140,000",
      "company": {
        "name": "Tech Solutions Inc.",
        "description": "A leading provider of innovative tech solutions.",
        "contactEmail": "contact@techsolutions.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "2",
      "title": "Frontend Engineer (Vue)",
      "type": "Part-Time",
      "description": "We are looking for a skilled Frontend Engineer with experience in Vue.js...",
      "location": "Remote",
      "salary": "$70,000 - $80,000",
      "company": {
        "name": "Creative Minds",
        "description": "A design agency focused on user experience.",
        "contactEmail": "jobs@creativeminds.com",
        "contactPhone": "555-123-4567"
      }
    }
  ]
}

Now, open a new terminal window and run the following command from your project's root directory:

npx json-server --watch db.json --port 8000

This command starts a server on http://localhost:8000. If you navigate to http://localhost:8000/jobs in your browser, you'll see the job data from your db.json file. We now have a working API to build against!


Fetching and Displaying Data (GET Request)

Our first task is to fetch the list of jobs from our API and display them on the page. To do this, we'll use the useEffect hook to trigger the API call when the component first renders. We'll also need state to manage the data itself, a loading indicator, and any potential errors.

The following video provides an excellent, comprehensive tutorial on this entire process.

Fetching Data in React - Complete Tutorial

This video from Cosden Solutions provides a fantastic walkthrough of the entire data-fetching process. We'll focus on setting up the initial fetch, managing a loading state, and handling potential errors.

Please watch from 01:50 to 15:31. This covers: Fetching Data (01:50 - 06:35): How to use fetch inside an async function within a useEffect hook to get data and set it to state. Loading State (06:35 - 09:44): Implementing an isLoading state to show a loading message to the user. Error Handling (09:44 - 15:31): Using a try...catch...finally block to manage errors and ensure the loading state is always correctly reset.

As you saw in the video, the core pattern involves three pieces of state and an effect:

import { useState, useEffect } from 'react';

function JobListings() {
  const [jobs, setJobs] = useState([]);
  const [loading, setLoading] = useState(true); // Start as true
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchJobs = async () => {
      try {
        const res = await fetch('http://localhost:8000/jobs');
        const data = await res.json();
        setJobs(data);
      } catch (error) {
        setError('An error occurred while fetching data.');
        console.error('Fetch error:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchJobs();
  }, []); // Empty array means this runs once on mount

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>{error}</div>;
  }

  return (
    <div>
      {/* Map over the 'jobs' state to render the list */}
      {jobs.map(job => (
        <div key={job.id}>{job.title}</div>
      ))}
    </div>
  );
}

A Deeper Look at Error Handling with fetch

The try...catch block with async/await is great, but it has a nuance with the fetch API. fetch only throws an error on network failures (e.g., you lose internet connection). It does not throw an error for bad HTTP responses like 404 Not Found or 500 Internal Server Error.

To handle these cases robustly, we must manually check the response.ok property, which is true for successful status codes (200-299). If it's false, we should throw our own error.

This next video from Net Ninja explains this critical distinction perfectly.

Full React Tutorial #19 - Handling Fetch Errors

The fetch API has a specific behavior with errors that's important to understand. This clip from Net Ninja clearly explains the two types of errors we need to handle: network errors and server response errors.

Watch from 00:00:02 to 07:39. Pay attention to: The .catch() block (00:00:02): How it catches network failures when using .then() syntax. Checking response.ok (01:51): How to manually check for bad HTTP responses and throw an error that will be caught by your catch block. Updating State (05:17): How to put it all together by setting error state and updating the UI.

Applying this more robust check, our fetchJobs function becomes:

const fetchJobs = async () => {
  try {
    const res = await fetch('http://localhost:8000/jobs');
    
    // Check if the response was successful
    if (!res.ok) {
      throw new Error('Failed to fetch data from the server.');
    }

    const data = await res.json();
    setJobs(data);
  } catch (error) {
    setError(error.message);
    console.error('Fetch error:', error);
  } finally {
    setLoading(false);
  }
};

This ensures your application can properly handle both network issues and server-side problems.


Sending Form Data to the API (POST Request)

Now for the exciting part: connecting the form we built in the last lesson. Instead of just updating local state, we'll modify our submission handler to send a POST request to the API, creating a new job listing.

To send data, we need to provide fetch with a second argument: an options object. This object will specify:

  • method: 'POST'
  • headers: To tell the server we're sending JSON data.
  • body: Our form data, converted into a JSON string using JSON.stringify().

Here is how you would modify the submission handler from the previous lesson. Assume addJobSubmit is the function passed down as a prop to your form component.

// In your form component (e.g., AddJobPage.jsx)
const submitForm = async (e) => {
  e.preventDefault();

  const newJob = {
    // ...all your form data from state
    title,
    type,
    location,
    // etc.
  };

  try {
    const res = await fetch('http://localhost:8000/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newJob),
    });

    if (!res.ok) {
        throw new Error('Failed to add job.');
    }
    
    // Call the parent's function to update global state or navigate
    // Or you could parse the response and use the newly created job object
    // const createdJob = await res.json();
    // addJobSubmit(createdJob);
    
    // For now, let's just assume success and navigate away
    // (You would need `useNavigate` from `react-router-dom` for this)
    alert('Job Added Successfully!');
    // navigate('/jobs');

  } catch (error) {
    alert(error.message);
  }
};

This function now takes the form data, sends it to our mock API, and handles success or failure. json-server will automatically add the new job to db.json and assign it a unique id.

React Application Displaying API Fetched Data
An example of a final UI where profile information, including name, bio, and location, has been successfully fetched from an API and rendered in a React component.

Practical Exercise: Full Integration

Now it's time to put all the pieces together in your application.

  1. Set up json-server: Create a db.json file with an array of items relevant to your project (e.g., jobs, blog posts, products) and start the server.
  2. Fetch Data on Load:
    • In the component responsible for displaying your list of items (e.g., JobListingsPage), implement the useEffect logic to fetch all items from your API when the component mounts.
    • Add loading and error states.
    • Use conditional rendering to display a loading message, an error message, or the list of fetched items.
  3. Connect Your "Add" Form:
    • Go to the component containing the form you built in the previous lesson (e.g., AddJobPage).
    • Modify its submission handler to make a POST request to your json-server endpoint.
    • Include loading and error handling within the submission logic as well. You might want to disable the submit button while the request is in flight.
    • On a successful submission, navigate the user back to the main list page to see their new addition.

Conclusion

Congratulations! You have now connected the frontend and "backend" of your application. You've transformed your app from a static, in-memory prototype into a dynamic application that can read and write persistent data. Mastering the fetch API within the useEffect hook, combined with robust loading and error handling, is a fundamental skill for any React developer.

Key Takeaways:

  • The useEffect hook is the primary tool for performing side effects like API calls when a component mounts or dependencies change.
  • Always manage three key states for API requests: data, loading, and error.
  • The fetch API requires you to manually check response.ok to handle HTTP errors (like 404 or 500), as it only rejects on network failures.
  • POST requests require a configuration object specifying the method, headers (like 'Content-Type': 'application/json'), and a JSON.stringify-ed body.
  • The finally block in a try...catch...finally statement is perfect for ensuring your loading state is always set back to false, regardless of success or failure.

Next Up:

Our data fetching logic is currently living inside our components. While this works, it can become repetitive if multiple components need the same data. In our next lesson, we will learn how to extract this logic into a reusable custom hook, cleaning up our components and making our code more modular and maintainable.