Call an API in JavaScript

JavaScriptBeginner
Practice Now

Introduction

In this lab, you will learn the essential skills for interacting with web APIs using modern JavaScript. You will explore the fetch API, a powerful and flexible tool built into web browsers, to make asynchronous network requests. The primary goal is to understand how to request data from a server, handle the response, and manage potential errors, forming a foundational skill for building dynamic web applications.

You will begin by making a basic GET request to retrieve data and will then learn how to handle the response, parse it as JSON, and display the fetched data within an HTML element. The lab also covers crucial aspects like implementing robust error handling, making POST requests to send data to a server, and authenticating your requests using an API key.

Make a Basic GET Request with the fetch API

In this step, you will learn how to make a fundamental API call in JavaScript: a GET request. We will use the fetch API, which is a modern, powerful, and flexible tool built into all modern web browsers and available in Node.js 18+ environments. It allows us to request resources from a server asynchronously.

The fetch function is promise-based, which means it returns a Promise that resolves to the Response to that request, whether it is successful or not. This allows us to handle the result of the asynchronous operation when it completes.

First, let's create a file to write our code. In the file explorer on the left side of your screen, create a new file named index.js in the ~/project directory.

We will use the JSONPlaceholder API for this example. It's a free, fake online REST API that's perfect for testing and prototyping. We'll request a single "todo" item.

Now, add the following code to your index.js file. This code defines the API endpoint URL and uses fetch to make a GET request.

// Define the API URL for a single todo item
const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";

// Make a GET request using the fetch API
fetch(apiUrl)
  .then((response) => {
    // The fetch function returns a promise.
    // The first .then() block receives the Response object.
    // We need to call the .json() method on the response to parse the body text as JSON.
    return response.json();
  })
  .then((data) => {
    // The second .then() block receives the parsed JSON data.
    console.log("Data fetched successfully:");
    console.log(data);
  })
  .catch((error) => {
    // The .catch() block will execute if any error occurs during the fetch operation.
    console.error("Error fetching data:", error);
  });

Let's break down the code:

  • apiUrl: We store the URL of the API endpoint we want to contact in a constant.
  • fetch(apiUrl): This initiates the GET request to the specified URL and returns a Promise.
  • .then(response => response.json()): When the Promise resolves, this function is called. The response object is not the actual JSON data but a representation of the entire HTTP response. We call the response.json() method to extract the JSON body content, which itself returns another Promise.
  • .then(data => { ... }): This second .then() handles the Promise returned by response.json(). The data parameter now contains the actual JSON object from the API. We log this data to the console.
  • .catch(error => { ... }): If the Promise is rejected at any point (for example, due to a network error), this block will catch the error and log it to the console.

To execute your script and see the result, open a new terminal in the WebIDE and run the following command:

node ~/project/index.js

You should see the fetched data printed to your console, which represents a single "todo" item from the API.

Expected Output:

Data fetched successfully:
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }

Handle the Response and Parse JSON Data

In this step, we will delve deeper into handling the response from an API call. When you use fetch, the server sends back a Response object. We'll explore what this object contains and how to correctly parse the JSON data from its body to use it in your application.

The Response object you receive in the first .then() block is not the data itself. It's a representation of the entire HTTP response, including status codes (like 200 for OK), headers, and the response body. The body is a stream of data, and to use it, you need to read it.

The fetch API provides several methods for this, such as .text() for plain text and .json() for parsing JSON data. The .json() method reads the response stream to completion and returns a new promise that resolves with the result of parsing the body text as a JavaScript object. This is why we chain a second .then() to work with the actual data.

Let's modify our ~/project/index.js file to demonstrate this. Instead of just logging the entire data object, we will access and log specific properties from it, showing that we have a regular JavaScript object to work with.

Update the content of your ~/project/index.js file with the following code:

// Define the API URL for a single todo item
const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";

fetch(apiUrl)
  .then((response) => {
    // The response.json() method parses the JSON body of the response
    // and returns a promise that resolves with the resulting JavaScript object.
    return response.json();
  })
  .then((data) => {
    // Now 'data' is a JavaScript object. We can access its properties.
    console.log("Successfully parsed JSON data:");
    console.log(`Todo Title: ${data.title}`);
    console.log(`Is Completed: ${data.completed}`);
  })
  .catch((error) => {
    // The .catch() block will execute if any error occurs during the fetch operation.
    console.error("Error fetching or parsing data:", error);
  });

In this updated code, the second .then() block now receives the data as a JavaScript object. We use template literals (the backtick ` syntax) to create strings that include the values of data.title and data.completed, demonstrating that the JSON has been successfully parsed.

Now, execute the script again in your terminal to see the new output:

node ~/project/index.js

You will see a more structured output, confirming that you have successfully accessed the properties of the parsed JSON object.

Expected Output:

Successfully parsed JSON data:
Todo Title: delectus aut autem
Is Completed: false

Display Fetched Data in an HTML Element

In this step, you will learn how to take the data fetched from an API and display it on a web page. While logging data to the console is useful for development, the ultimate goal is often to present this information to the user. This requires an HTML file to structure the content and JavaScript to manipulate the Document Object Model (DOM).

First, we need an HTML file. In the file explorer on the left, create a new file named index.html in the ~/project directory.

Add the following basic HTML structure to your index.html file. This file includes a heading and a div element with the ID data-output, which will serve as a container for our fetched data. The <script> tag at the end of the body ensures our JavaScript runs after the HTML elements have been loaded.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>API Data Display</title>
  </head>
  <body>
    <h1>Fetched Todo Item</h1>
    <div id="data-output">
      <p>Loading data...</p>
    </div>
    <script src="index.js"></script>
  </body>
</html>

Next, you need to modify your ~/project/index.js file to interact with this HTML. Instead of logging to the console, the script will find the div element by its ID and update its content with the data from the API.

Replace the content of ~/project/index.js with the following code:

const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";

// Select the HTML element where we will display the data
const outputElement = document.getElementById("data-output");

fetch(apiUrl)
  .then((response) => response.json())
  .then((data) => {
    // Once we have the data, we update the HTML content.
    // We use innerHTML to replace the "Loading..." message with structured data.
    outputElement.innerHTML = `
      <p><strong>Title:</strong> ${data.title}</p>
      <p><strong>Completed:</strong> ${data.completed}</p>
    `;
  })
  .catch((error) => {
    // If an error occurs, display an error message to the user.
    outputElement.textContent = "Failed to load data.";
    console.error("Error fetching data:", error);
  });

Now, to see the result, you need to serve these files through a web server. The LabEx environment includes Python, which has a simple built-in web server.

  1. Open a new terminal in the WebIDE.
  2. Start the web server by running the following command. This will serve the files in the current directory (~/project) on port 8080.
python3 -m http.server 8080
  1. The LabEx platform will detect this running service and provide a "Web 8080" Tab to preview your index.html page.

You should now see your web page displaying the title and completion status of the fetched "todo" item, replacing the initial "Loading data..." message.

Implement Error Handling for API Calls

In this step, we will focus on making our API call more robust by implementing proper error handling. API requests can fail for various reasons, such as a wrong URL, network issues, or server-side problems. It's crucial to handle these potential failures gracefully to provide a better user experience.

A key detail about the fetch API is that its promise does not reject on HTTP error statuses like 404 (Not Found) or 500 (Internal Server Error). It only rejects if there's a network failure that prevents the request from completing. To handle HTTP errors, we need to check the response.ok property, which is true for successful responses (status codes 200-299).

Let's update our ~/project/index.js to check the response status and handle potential errors.

Replace the content of your ~/project/index.js file with the following code. We are adding a check inside the first .then() block.

const apiUrl = "https://jsonplaceholder.typicode.com/todos/1";
const outputElement = document.getElementById("data-output");

fetch(apiUrl)
  .then((response) => {
    // Check if the response was successful.
    // The 'ok' property is a boolean that is true if the status code is in the 200-299 range.
    if (!response.ok) {
      // If not, we throw an error which will be caught by the .catch() block.
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    outputElement.innerHTML = `
      <p><strong>Title:</strong> ${data.title}</p>
      <p><strong>Completed:</strong> ${data.completed}</p>
    `;
  })
  .catch((error) => {
    // Display a user-friendly error message in the HTML element.
    outputElement.textContent = "Failed to load data. Please try again later.";
    // Log the technical error to the console for debugging purposes.
    console.error("Error fetching data:", error);
  });

To see our error handling in action, let's intentionally use an invalid API URL. Modify the apiUrl in ~/project/index.js to point to a resource that doesn't exist. This will cause the API to return a 404 Not Found error.

Change this line in your index.js file:
const apiUrl = 'https://jsonplaceholder.typicode.com/todos/1';

To this:
const apiUrl = 'https://jsonplaceholder.typicode.com/invalid-path/1';

Now, let's see the result. If your Python web server from the previous step is still running, simply refresh the preview tab in your browser. If you stopped it, start it again from the terminal:

python3 -m http.server 8080

Then, open the preview. Instead of the data, you should see the error message displayed on the page because our if (!response.ok) check caught the 404 error.

Expected Output on the Web Page:

Failed to load data. Please try again later.

Important: Before moving to the next step, remember to change the apiUrl back to the correct one: https://jsonplaceholder.typicode.com/todos/1.

Make a POST Request to Send Data

In this step, you will learn how to send data to a server using a POST request. So far, we have only fetched (or "GET") data. A POST request is used to submit data to a specified resource, often causing a change in state or a new entry to be created on the server.

To do this, we need to provide more information to the fetch function, including the request method, headers to describe the data we're sending, and the data itself in the request body.

First, let's update our ~/project/index.html file to include a form. This will allow us to input data that we can then send to the API. Replace the entire content of index.html with the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>POST Request Example</title>
  </head>
  <body>
    <h1>Create a New Todo Item</h1>
    <form id="add-todo-form">
      <input
        type="text"
        id="todo-title"
        placeholder="Enter a new todo title"
        required
      />
      <button type="submit">Add Todo</button>
    </form>
    <hr />
    <h2>Server Response:</h2>
    <div id="response-output"></div>

    <script src="index.js"></script>
  </body>
</html>

This HTML creates a simple form with a text input and a submit button, plus a div to display the server's response.

Next, we will completely replace the code in ~/project/index.js to handle the form submission and make a POST request.

Replace the content of ~/project/index.js with the following code:

// The API endpoint for creating new todos
const apiUrl = "https://jsonplaceholder.typicode.com/todos";

// Get the form and the response output element from the DOM
const todoForm = document.getElementById("add-todo-form");
const responseOutput = document.getElementById("response-output");

// Add an event listener for the form's submit event
todoForm.addEventListener("submit", function (event) {
  // Prevent the default form submission behavior
  event.preventDefault();

  // Get the title from the input field
  const todoTitle = document.getElementById("todo-title").value;

  // The data we want to send in the POST request
  const newTodo = {
    title: todoTitle,
    completed: false,
    userId: 1
  };

  // The options for the fetch request
  const requestOptions = {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(newTodo)
  };

  // Make the POST request
  fetch(apiUrl, requestOptions)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      // The API returns the created object, including a new 'id'
      return response.json();
    })
    .then((data) => {
      // Display the server's response in our output div
      responseOutput.innerHTML = `<p>Successfully created todo!</p><pre>${JSON.stringify(data, null, 2)}</pre>`;
    })
    .catch((error) => {
      responseOutput.textContent = "Failed to create todo.";
      console.error("Error:", error);
    });
});

Let's break down the new requestOptions object:

  • method: 'POST': This tells fetch to perform a POST request.
  • headers: { 'Content-Type': 'application/json' }: This header informs the server that the data in the body is in JSON format.
  • body: JSON.stringify(newTodo): This is the actual data we are sending. It must be converted into a JSON string before being sent.

Now, start your web server again if it's not running:

python3 -m http.server 8080

Open the preview, type a title for a new todo item into the input field, and click the "Add Todo" button. You should see a success message and the data returned by the server, which includes the new id for the item you "created".

Authenticate Requests with an API Key

In this step, you will learn how to authenticate your API requests using an API key. Many APIs require authentication to identify the user, control access to data, and track usage. An API key is a unique string of characters that you include with your request to prove you have permission to use the API.

There are several ways to send an API key, but a common and secure method is to include it in the request headers, typically using the Authorization header.

For this example, we will simulate fetching protected user data that requires an API key. We will modify our code to include an Authorization header with a "Bearer" token, which is a standard way to send authentication credentials.

First, let's simplify our ~/project/index.html file to just display the fetched user data. Replace its content with the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Authenticated API Request</title>
  </head>
  <body>
    <h1>User Profile (Protected)</h1>
    <div id="user-profile">
      <p>Loading user data...</p>
    </div>
    <script src="index.js"></script>
  </body>
</html>

Next, replace the content of ~/project/index.js with the code below. This script will make a GET request to fetch a user's data and include a fake API key in the headers.

// Define the API URL for a user's profile
const apiUrl = "https://jsonplaceholder.typicode.com/users/1";

// In a real application, you would get this key from your API provider.
const apiKey = "YOUR_SECRET_API_KEY_HERE";

// Select the HTML element for output
const userProfileElement = document.getElementById("user-profile");

// Create the request options object, including the headers for authentication
const requestOptions = {
  method: "GET",
  headers: {
    Authorization: `Bearer ${apiKey}`
  }
};

// Make the authenticated GET request
fetch(apiUrl, requestOptions)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    // Display the fetched user data
    userProfileElement.innerHTML = `
      <p><strong>Name:</strong> ${data.name}</p>
      <p><strong>Email:</strong> ${data.email}</p>
      <p><strong>Website:</strong> ${data.website}</p>
    `;
  })
  .catch((error) => {
    userProfileElement.textContent = "Failed to load user profile.";
    console.error("Error:", error);
  });

In this code:

  • We define a placeholder apiKey.
  • We create a requestOptions object.
  • Inside headers, we add an Authorization key. The value Bearer ${apiKey} is a common format, where "Bearer" is the token type, followed by the key itself.

Note: The JSONPlaceholder API we are using is public and doesn't actually require an API key. It will simply ignore this header. However, this code demonstrates the standard method you would use for the many real-world APIs that do require authentication.

To see the result, start your web server if it's not already running:

python3 -m http.server 8080

Then, open the preview. The page will successfully load and display the user's profile, demonstrating that you have correctly structured an authenticated API request.

Summary

In this lab, you learned how to call an API in JavaScript using the modern fetch API. You started by making a basic GET request to retrieve data from a public API endpoint. You practiced handling the asynchronous nature of fetch using promises, chaining .then() blocks to process the response and parse the body text as JSON. Key skills included displaying the fetched data dynamically within an HTML element and implementing robust error handling with a .catch() block to manage potential network failures.

Building on these fundamentals, you explored how to send data to a server by constructing and executing a POST request, including the necessary headers and a JSON payload. Finally, you learned a common method for securing API communications by authenticating your requests, which involved including an API key to gain authorized access to protected resources.

close