Module 3: HTTP: The Native Fetch - Web Unit 3 Sprint 11

Understanding Fetch

This Objective covers making basic requests with the native fetch API in JavaScript. Fetch provides a modern, promise-based mechanism to interact with HTTP resources. It's widely supported in modern browsers and offers a more flexible, powerful alternative to older methods like XMLHttpRequest.

How to Build It

What is fetch

Fetch is a native Web API that serves as a modern replacement for XMLHttpRequest, providing a more powerful and flexible way to make network requests.

Unlike Axios, which is a library that adds a layer of abstraction and additional features, fetch is built into the browser environment, ensuring that no extra dependencies are required. Its promise-based nature simplifies handling asynchronous operations, allowing developers to write cleaner, more readable code than with the ancient XMLHttpRequest.

Fetch not only supports a variety of request types but also allows extensive customization through settings for headers, credentials, and more, making it adaptable to complex scenarios.

Using fetch to resolve a Response object

When using fetch, it is essential to understand that it returns a promise that resolves as soon as the server responds with headers, even before the full response body is ready. This design allows developers to start processing parts of the response (like checking the status code) without waiting for the entire payload, which can be particularly beneficial when dealing with large datasets.

The Response object provides several properties and methods that can be used to inspect and manipulate the incoming data. For example, developers can asynchronously read the body content in various formats (like JSON) depending on the nature of the response, enabling efficient data handling.

The basic syntax for using fetch is:

fetch('http://example.com/api/cats') // fetch defaults to GET
  .then(response => {
    // inspect the Response object (headers, status)
    // ❗ note that the data is not available here,
    // as the promise fulfills _before_ the response body arrives
  })
  .catch(error => {
    // handle any errors
  })

Studying the status code and throwing an error if the request did not go OK

One of the key differences between fetch and other HTTP request tools like Axios is how they handle HTTP error statuses. Fetch does not automatically reject the promise when the server responds with an error status (like 404 or 500). Instead, it provides an ok property that indicates whether the response was successful (status in the range 200-299).

This approach requires developers to explicitly check this property and manually throw errors if needed. This pattern gives developers more control over error handling, allowing them to decide how to respond to different server statuses effectively, but it also means that we must throw an error manually, if we intend to centralize all failures inside the catch.

Here's how you can throw errors for unsuccessful HTTP responses:

fetch('http://example.com/api/cats')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP status ${response.status}`)
    }
  })
  .catch(error => {
    console.error('There was a problem:', error)
  })

Reading the Content-Type header of the response

After ensuring the HTTP request was successful, it's often necessary to examine the Content-Type header of the response. This header indicates the type of data that is being returned by the server, allowing your application to handle it appropriately. Whether the data format is JSON, plain text, HTML, or something else, knowing the content type helps you parse the response correctly.

Here's how you can check the Content-Type of the response and take appropriate actions based on its value:

fetch('https://example.com/api/cats')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP status ${response.status}`)
    }
    const contentType = response.headers.get('Content-Type')
    
    if (contentType.includes('application/json')) {
      // Handle JSON data
    }
  })
  .catch(error => {
    console.error('There was a problem:', error)
  })

Example of using fetch in a React application

Here is a working example you can build yourself, by scaffolding a React application using @bloomtools/react (version 0.1.39 or later):

import React, { useEffect } from 'react'

export default function App() {
  useEffect(() => {
    function getCats() {
      fetch('http://localhost:3003/api/cats')
        .then(res => {
          if (!res.ok) {
            throw new Error(`Ouch, status ${res.status}`)
          }
          const contentType = res.headers.get('Content-Type')
          if (contentType.includes('application/json')) {
            console.log('We are getting cats... but they have not arrived yet!')
          }
        })
        .catch(err => {
          console.log('Something went wrong GETing cats', err)
        })
    }
    getCats()
  }, [])
  return (
    

Hello, Cats!

) }

Thanks to fetch, we are able to perform network requests with a modern, promise-based tool that simplifies asynchronous HTTP communication. This API is essential for front-end developers to understand as it interacts with APIs and performs network operations in modern web applications.

In the next Objective you will learn how to actually read the JSON data that returns from this GET request!

Pasing Fetch Responses

In this Objective, we will explore parsing the response body when using the fetch API in JavaScript. Understanding how to handle different data formats and manage potential errors during fetch operations is crucial for any modern web developer. This chapter will guide you through various techniques for effectively retrieving and utilizing data from server responses.

How to Build It

Parsing the Response Body

As we know, fetch fulfills its promise as soon as the response starts arriving, and does not wait for the full response body to arrive. This allows for quick decision making, even if the payload is very big.

In the following example we are GETing cats from an API, and throwing an error if the status code is not in the 200 range.

import React, { useEffect } from 'react'

export default function App() {
  useEffect(() => {
    function getCats() {
      fetch('http://localhost:3003/api/cats')
        .then(res => {
          if (!res.ok) {
            throw new Error(`Ouch, status ${res.status}`)
          }
        })
        .catch(err => {
          console.log('Something went wrong GETing cats', err)
        })
    }
    getCats()
  }, [])
  return (
    <div>
      <h2>Hello, Cats!</h2>
    </div>
  )
}

Knowing the format of the data you're expecting from your API is essential for parsing the response correctly. If the Content-Type of the response is known and consistent, you can directly parse the data as JSON by returning res.json(), which gives a new promise:

fetch('http://localhost:3003/api/cats')
  .then(res => {
    if (!res.ok) {
      throw new Error(`Ouch, status ${res.status}`)
    }
    // let's parse the response body as JSON
    return res.json() // res.json returns a promise
  })
  .then(cats => {
    // the response body has been parsed
    console.log(cats) // The cats array has arrived!
  })
  .catch(err => {
    console.log('Something went wrong GETing cats', err)
  })

Other Response Body Formats

Fetch also provides methods like res.blob() for binary data and res.text() for text data, which are used depending on the content type of the response.

If we do not know what content type will arrive, we must check the Content-Type header, which we have immediate access to:

// Determine how to parse the response based on the Content-Type
const contentType = res.headers.get('Content-Type')
if (contentType.includes('application/json')) {
  return res.json()
} else if (contentType.includes('text/html')) {
  return res.text()
} else if (contentType.includes('image')) {
  return res.blob()
} else {
  throw new Error('Unsupported content type: ' + contentType)
}

Using async/await

Async/await syntax simplifies handling asynchronous operations by making the code appear more synchronous and linear. Here's how you can use async/await with fetch in a React component:

import React, { useEffect } from 'react'

export default function App() {
  useEffect(() => {
    async function getCats() { // don't forget the async keyword
      try {
        const res = await fetch('http://localhost:3003/api/cats')
        if (!res.ok) throw new Error(`Ouch, status ${res.status}`)
        const cats = await res.json() // we have the cats now!
      } catch (err) {
        console.log('Something went wrong GETing cats', err)
      }
    }
    getCats()
  }, [])
  return (
    <div>
      <h2>Hello, Cats!</h2>
    </div>
  )
}

In the next Objective you will learn how to make POST, PUT and DELETE requests with Fetch!

Configuring Fetch Requests

In this Objective, we will use the fetch API in JavaScript to make more complex HTTP requests like POST, PUT, and DELETE. We will also explore how to customize these requests using the second argument of fetch to specify the method, body, and headers.

This is essential for any web developer looking to perform CRUD (Create, Read, Update, Delete) operations in modern web applications. This objective will provide a step-by-step guide on how to construct these requests effectively.

How to Build It

Configuring Fetch for Different HTTP Methods

The fetch function in JavaScript not only retrieves data but can also be used to send data to servers. By default, fetch makes a GET request, but it can be configured to perform other types of requests such as POST, PUT, and DELETE through its second argument. This argument is an object that lets you control a number of parameters like method, body, and headers.

Making a POST Request

To create a new resource on a server, we typically use a POST request. Unlike GET requests, which do not include a request body, POST requests involve sending data from the client to the server. This data is often in JSON format and constitutes the body of the request.

When using POST with the fetch API, it's important to correctly configure the request's body and headers. JavaScript objects must be converted to a JSON string using JSON.stringify() before they can be sent, as the server expects JSON formatted text. Additionally, you must explicitly set the Content-Type header to 'application/json'. If this header is not set, fetch may incorrectly default to sending the data as plain text.

Here is an example of how to use fetch to send data in a POST request:

// fetch takes a second argument with configuration
fetch('http://localhost:3003/api/cats', {
  method: 'POST', // the method of the request is set here
  body: JSON.stringify({ // you must stringify your payload
    name: "Whiskers",
    breed: "Siamese"
  }),
  headers: {
    // this is important if we intend to send JSON
    'Content-Type': 'application/json'
  }
})
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error))

Updating Data with a PUT Request

A PUT request is used to update an existing resource on the server entirely with the data you provide. When performing a PUT operation, the payload typically (although not always) represents the updated state of the entire resource, including the properties that stay the same.

Below is an example of how you might update a resource:

// let's change the name of the cat with ID 4
fetch('http://localhost:3003/api/cats/4', { // note the ID at the end of the URL
  method: 'PUT', // we replace items on the server using PUT
  body: JSON.stringify({ // same as with POST, you must stringify your payload
    name: "Freddy",  // the property that changes
    breed: "Siamese", // the property that stays the same
  }),
  headers: {
    // as with POST, if we send JSON we need to set the correct header
    'Content-Type': 'application/json'
  }
})
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error))

Deleting Data with a DELETE Request

To remove a resource from the server, you can use a DELETE request. The example below shows how to send a DELETE request:

// let's delete the cat with ID 4
fetch('http://localhost:3003/api/cats/4', { // note the ID at the end of the URL
  method: 'DELETE', // method used to remove resources from the server
})
  .then(response => response.json())
  .then(data => console.log('Success:', data)) // if we care about the response data
  .catch(error => console.error('Error:', error))

Wrap-Up: The Importance of Understanding the Fetch API

As we conclude our module on the Fetch API, it's important to reflect on why mastering this fundamental tool is essential for web developers, even in an era where numerous higher-level libraries and frameworks exist to simplify AJAX requests.

  1. Understanding Fundamentals: The Fetch API provides a direct, powerful way to handle HTTP requests and responses. It's built into the browser, which means understanding Fetch allows developers to work with every aspect of network interactions using native methods. This foundational knowledge gives you a clearer understanding of how data is exchanged between clients and servers, enabling you to troubleshoot and optimize interactions more effectively.
  2. Flexibility and Control: While libraries like Axios or jQuery's AJAX methods offer convenience and additional features, they abstract some of the lower-level functionalities. In contrast, the Fetch API offers granular control over network requests, including the ability to work directly with Promises, stream responses, and finely configure requests and responses. This can be particularly advantageous when dealing with complex scenarios that require detailed manipulation of headers, request methods, or payloads.
  3. Modern Features: Fetch supports modern JavaScript features like async/await out of the box, making it a natural fit for contemporary web development practices. It handles a wide range of data formats and can be used in both traditional multi-page applications and modern single-page applications.
  4. No Additional Dependencies: Using Fetch eliminates the need for additional libraries, reducing the overhead and complexity of managing dependencies in a project. This not only simplifies the development process but also minimizes potential compatibility issues and decreases the load time of web applications.
  5. Future-Proofing Skills: The Fetch API is part of the living standard, actively maintained and updated. Learning and using Fetch ensures that your skills remain relevant as web standards evolve. As new features are added to the specification, knowing Fetch allows you to adopt them quickly, keeping your applications up to date with the latest in web technology.
  6. Broad Applicability: Understanding how to use the Fetch API enhances your ability to interact with numerous APIs and web services, a critical skill in today's interconnected digital landscape. Whether you are fetching simple data or integrating complex third-party services, the versatility of Fetch is a valuable asset.

While libraries and frameworks will come and go, the underlying technology they abstract remains critical. By understanding how to use the Fetch API effectively, you equip yourself with a skill that ensures deeper insights into web development, better performance optimization, and a more robust handling of network interactions in your applications.

Prompt Engineering with Fetch

Welcome to this Learning Objective on using JavaScript's fetch function to interact with the OpenAI API. This section is designed to not only practice the mechanics of making network requests but also to demonstrate how these capabilities are applied in practical scenarios such as interacting with cutting-edge AI technologies.

Given the costs associated with API usage, following along without executing the code is completely fine. However, engaging directly will provide the best learning experience.

How to Build It

Prerequisites

  • Node.js Version: Ensure your version of Node.js is at least 20.6.0.
  • OpenAI API Key: The OpenAI API is not free, and you will need an API key to make requests. If this is something you'd like to try out, sign up at OpenAI's website and obtain an API key.

Setting Up the Project

Your Walkthrough Video below will walk you through a more realistic project organization. For right now, you will write all of your code inside a single JavaScript module called index.mjs. Mind the mjs extension!

// index.mjs

const OPENAI_API_KEY = "your api key here" // do not push this file to GitHub!
console.log(OPENAI_API_KEY)

In order to execute our program, instead of loading it in a browser, you will simply use Node from your command line:

# Navigate your Terminal to the location of index.mjs

node index.mjs # This executes your code

You should see your API key printing to the Terminal!

Writing the Fetch Code

We will begin by creating a new variable holding the address of the endpoint we intend to fetch:

// index.mjs

const OPENAI_API_KEY = "your api key here"
const OPEN_AI_URL = 'https://api.openai.com/v1/chat/completions'

Underneath these two constants, we will start a try/catch block, and POST a request to the OpenAI API:

try {
  const res = await fetch(OPEN_AI_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  })
  if (!res.ok) throw new Error(`Something is wrong: ${res.status}`)
} catch (err) {
  console.log(err.message)
}

This call is not going to succeed. The request is malformed for two reasons. First, we are not supplying the API key token. And second, we are not providing any payload containing the prompt. See for yourself by running node index.mjs in your Terminal! You should see a 401 error message printing in the Terminal.

Using the OpenAI API Token

Let's fix the authorization issue by adding an Authorization header to the request, containing the token:

try {
  const res = await fetch(OPEN_AI_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${OPENAI_API_KEY}`, // Here is your token!
    },
  })
  if (!res.ok) throw new Error(`Something is wrong: ${res.status}`)
} catch (err) {
  console.log(err.message)
}

Adding a Payload of JSON

Now, we will add the payload which the API expects. It will specify the Large Language Model we wish to use, and an array of messages. The first one provides initial setup and context for the model, and the second one is the query proper. Then we will make sure to parse the response body as JSON.

try {
  const res = await fetch(OPEN_AI_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${OPENAI_API_KEY}`
    },
    body: JSON.stringify({ // The headers say you are sending JSON
      model: 'gpt-3.5-turbo', // The model you are using today
      messages: [
        {
          role: 'system',
          content: 'Answer as Captain Haddock' // Context for the model
        },
        {
          role: 'user',
          content: 'What is our place in this universe?' // The user query
        }
      ],
    })
  })
  if (!res.ok) throw new Error(`Something is wrong: ${res.status}`)
  const data = await res.json()

  // You are interested in a particular part of the response payload
  console.log(data.choices[0].message.content) // Here is the generated output
  // But feel free to explore the entire response body!
  console.log(data)
} catch (err) {
  console.log(err.message)
}

Take it for a drive and see the generated output, as well as the full response from the API.

This was your first taste of Prompt Engineering. A brave new world awaits!

Module 3 Project: The Native Fetch

This project will have you build an app for a dog shelter. The app has two screens: a route that lists the dogs, and another one that renders a form used to add, edit and delete dogs. You will need all of your skill for this one, but the rewards will be great! By the end of it you will have practiced using the Native fetch to perform full CRUD on a backend resource (Create, Read, Update and Delete). On top of that you will brush up on React Router, state and props management, component reusability (using the same form for create and edit operations) and more.

The Module Project contains advanced problems that will challenge and stretch your understanding of the module's content. The solution video is available below in case you need help or want to see how we solved each challenge (note: this challenge can be solved in many ways). If you can successfully complete all the Module Projects in a sprint, you are ready for the Sprint Challenge and Assessment.

Instructions

The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:

Starter Repo: The Native Fetch

  • Fork the repository,
  • clone it to your machine, and
  • open the README.md file in VSCode, where you will find instructions on completing this Project.
  • submit your completed project to the BloomTech Portal

Solution