NextJS Course (3/9) – Connecting with APIs and Getting Data

Now that we have a basic site with two pages, it’s time to look at how we can get some content onto these pages. We don’t just want to type some text on there and have static content, we want to be able to pull in data from other sources and display dynamic content based on user input.

Before we get into really cool APIs and integrate AI into our app, there are some basic concepts and principles we must learn. The first step to doing that is to learn how to connect to a very simple API and fetch some data, so let’s get started and build from there.

Fetching data from an API

For the first example, we’ll use a free fake API that is provided for testing and provides some dummy JSON data. Go to https://jsonplaceholder.typicode.com/todos in your browser and you’ll see something like this:

Now it may not look this neatly formatted on your screen, as that is the work of a browser extension, but you should see the same data. This is the data that we’re going to fetch into our website and display on the page.

First, open up your page.tsx file in the blockbuster_chat folder. There’s not much in here currently so we’re basically going to start from scratch. Just remove everything and let’s start anew:

import React from 'react'

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

We import React as we always do, and now we define an interface just like last time because we know we’re going to have to deal with data in this particular format. This is simply a template that matches the data keys and types that we saw on the website just a moment ago.

The function standard framework is as follows, which is the same as it has always been so far:

const BlockBusterChat = () => {
  return (
    <div>
        BlockBusterChat
    </div>
  )
}

export default BlockBusterChat

Notice that we can take some action though after the { opens and before the return() statement begins. Before we return anything, we need to fetch some data, so let’s do that now (again don’t worry about the red squiggly lines, we’ll get to it!):

const BlockBusterChat = () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/todos')
  const todo_list: Todo[] = await res.json()

  return (
    <div>
        BlockBusterChat
    </div>
  )
}

export default BlockBusterChat

We added two lines of code to run before we get to our return statement. We define a const named res which is short for response, and we use the fetch function to get the data from the URL we provided. As fetching data from an API requires a bit of time, the fetch function returns what JavaScript calls a promise.

This simply means that JavaScript is promising to give us the data as soon as it arrives. We use the await keyword to wait for the data to arrive, assigning it to the res constant before we move on to the next line of code.

The res constant object now contains the raw data received from the API, but it’s not in a format that we can use yet. We need to convert it to a format that we can work with, and that’s where the res.json() function comes in. This function parses the data into an object that we can work with, and we assign it to the todo_list constant.

The : Todo[] part is a type assertion, which tells TypeScript that the todo_list constant is an array of Todo objects. The reason we use the await keyword again is that the res.json() function also returns a promise, so we need to wait for it to resolve before we can use the data.

You’ll notice the red lines in your code editor:

This is happening because we used the await keyword inside a normal function. We need to make the function async or asynchronous to be able to use the await keyword. So let’s do that now:

const BlockBusterChat = async () => {
  const res = ...

All you have to do is add the async keyword before the () => part of the function definition. This tells JavaScript that the function is asynchronous and that it contains await keywords that need to be resolved before the function can continue.

Mapping over the data

Ok so now we have an array of Todo objects in the todo_list constant, but we’re not doing anything with it yet. This is where we make the link with the return() portion of the function. Let’s display it on the page:

const BlockBusterChat = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/todos')
  const todo_list: Todo[] = await res.json()

  return (
    <>
      <h1 className='font-bold'>Blockbuster Chat</h1>
      <ul>
        {todo_list.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  )
}

export default BlockBusterChat

The first thing I want to point out is the <> and </> tags are the outer-most ones inside the return statement. These are called React fragments and they allow us to return multiple elements from a component without having to wrap them in a single parent element.

If you try to return and <h1> and a <ul> element that is two different elements, and you’ll get an error. The fragment acts as a container making it into a single element which is a requirement for React.

We then simply have a heading h1 where we use the font-bold class from Tailwind CSS to make it bold and display some heading text. After that is our ul element which is a list element where we display the list of todos we fetched.

The following might be confusing if you haven’t seen this before, so let’s peel off the layers one by one:

  • The outer {} brackets are again just to switch into JavaScript mode inside the return statement.
  • The todo_list.map() function is a JavaScript function that iterates over each item in the todo_list array and returns a new array with the results of the function we provide.
  • The map() function takes a function as an argument, which is called for each item in the array. We use an arrow function todo => () to define the function. It’s a bit like Python and saying for todo in todo_list: do something.
  • So the input to the function inside map() is todo, which we then take => and we return when is inside the next () brackets. This is the JSX code that will be returned for each item in the array.
  • We return a <li> item and show the todo.title for each todo inside the list.
  • We use the key attribute to give each list item a unique key. This is a requirement in React and helps React keep track of which items have changed, been added, or been removed. We can just use the id field from the todo object as the key as they are unique.

If you now refresh the page you will see a list:

Now let’s fix it up a bit, change the content of the <ul>:

    {todo_list.map(todo => (
        <li key={todo.id}>{todo.id}. {todo.title} - {todo.completed ? 'Completed' : 'Not completed'}</li>
    ))}

We added a bit more information to each list item. We show the id of the todo, then the title, and then we check if the completed field is true or false and display a message accordingly using Javascript’s ternary operator. (statement or variable ?(if) return this if true :(else) return this if false)

Awesome, you now have live data on your website!

The issue with API keys

Now that we have a basic understanding of how to fetch data from an API, let’s talk about the problem here. When you fetch data from an API it is usually a real one that requires you to send your API key. We don’t want to expose our API key to the world as anyone could use it then! So using the method above to send a request straight to ChatGPT or any other API is not a good idea.

So how should we handle this? We need to make the request from the server side, and then send the data to the client side. This way the API key is never exposed to the client side and is kept safe on the server side. Luckily Next.js can handle both server-side and client-side code, so we can make use of this to keep our API key and other details (like prompt setups!) safe.

Creating our own APIs

What we’re going to do is create an API route in our Next.js project that will make the request to the ChatGPT API. The client can then send its request to this API route, which runs on the server, and receive the data back from there. This way the API key is never exposed to the client side. So let’s first take a look at how we can create an API route and have a clear division between server and client-side code, so that we can be ready to really start adding something cool to our website in the next part!

For now, we’re going to stick with the to-do list example, as this will allow for a gradual buildup of complexity. Hang in there, we will get to the cool stuff soon but we need to build a solid foundation first!

First, we need to create a new folder in our πŸ“ app folder named api. Then inside, create another folder named blockbuster, and a file named route.tsx to finish it off:

πŸ“ app
    πŸ“ api    ✨ New folder
        πŸ“ blockbuster   ✨ New folder
            πŸ“„ route.tsx  ✨ New file
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx      
    πŸ“„ page.tsx

Note that this time we did not create a page.tsx file in the blockbuster folder. This is because we are not going to be rendering any pages from this folder, but rather creating an API route. In this case Next.js is expecting a route.tsx file from us with the details for our API.

Open up the route.tsx file and add the following imports up top:

import { NextRequest, NextResponse } from "next/server";

NextRequest and NextResponse are objects that we can use to handle incoming requests and send responses back to the client. We will use these objects to handle the incoming request to our API, do whatever we want to do in between to get the data we need, and then send a response back to the client.

In this case, I’m just going to hardcode some data for us to return, so go ahead and copy the following constant into your file from the written tutorial (or copy some from the typicode URL we used earlier):

const todos = [
  {
    "userId": 1,
    "id": 1,
    "title": "Remember to feed Rex his daily mountain range",
    "completed": false
  },
  {
    "userId": 1,
    "id": 2,
    "title": "Convince Rex that the couch is not a chew toy",
    "completed": false
  },
  {
    "userId": 1,
    "id": 3,
    "title": "Find a way to stop Rex from chasing squirrels up trees",
    "completed": false
  },
  {
    "userId": 1,
    "id": 4,
    "title": "Train Rex to fetch a car",
    "completed": true
  },
  {
    "userId": 1,
    "id": 5,
    "title": "Figure out how to fit a T-Rex into a bathtub",
    "completed": false
  },
  {
    "userId": 1,
    "id": 6,
    "title": "Find a bigger house (or a smaller T-Rex)",
    "completed": false
  },
  {
    "userId": 1,
    "id": 7, "title": "Hide all the power outlets",
    "completed": false
  }
]

Ok, now that we have some fake data we need to create a route in our file that will accept requests and send back a response in return. Add the following code to your file:

export function GET(request: NextRequest) {
  return NextResponse.json(todos);
}

We export a function to handle GET requests (which is basically just getting a URL like visiting a page in your browser, without posting any data). Next.js will see to it that when this function gets called it provides us with the request object which is the input argument for our function (NextRequest is the type for TypeScript to know what to expect).

The function itself is extremely simple for now. We just instantly return a NextResponse object, calling the json() method on it to simulate a JSON response like the Typicode API we used earlier. We pass in our todos constant to this method, which will be sent back to the client as a JSON response.

Your route file should now look like this:

import { NextRequest, NextResponse } from "next/server";

const todos = [ ... ]

export function GET(request: NextRequest) {
  return NextResponse.json(todos);
}

Go ahead and save the file and then open up your browser window. Now go to http://localhost:3000/api/blockbuster and you should see the JSON data we just hardcoded in the route.tsx file. There is also a chance you see an error like this though:

What is going on? Well, if you see this or any other error after making large changes, just try restarting the dev server running Next.js. After making large changes, sometimes the server gets confused and needs a restart to get back on track. Take note to remember this as 90% of all errors you will encounter during the tutorial series can be fixed in this way, and you will probably need it several times during the course, which will save you a lot of time.

Press Ctrl + C in the terminal where you started the server to stop it, and then run npm run dev again to start it back up. Now try to refresh the page and see if the error is gone. You should now see your JSON data:

Again, yours might not be formatted as nicely as mine (I use a browser extension to format JSON data), but you should see the same data. We now have a basic API route that returns JSON data!

Time to head back to our page.tsx file in the blockbuster_chat folder and make use of this API route to fetch the data and display it on our page. We’re going to make this into a proper client-side thing though, so we need to learn two new concepts before we can really get started. React State and useEffect.

React State

React state is a way to store data in a component (on the client side/browser) that can change over time. When the state of a component changes, React will automatically re-render the component to reflect the new state. This allows us to create dynamic and interactive user interfaces.

To use state in a functional component, we use the useState hook provided by React and pass in some kind of default value for our state variable.

useState(default_value)

The useState hook returns an array with two elements: the current state value and a function to update the state value. We can destructure this array to get the current state value and the function to update the state value.

const [count, setCount] = useState(0);

In this example, we create a state variable count with an initial value of 0, and we get a function setCount that we can use to update the count state. We can update the count variable by using the setCount function and passing a new value:

setCount(count + 1);

Here we updated the count state by setting it to whatever the current value of count is + 1. React will automatically re-render the component to reflect the new state value. One important thing for this all to work properly is that you never update the state variable directly (like count = count + 1), always use the function provided (setCount(count + 1)).

Here’s an example of how to use state in a functional component:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

In this example, we use the useState hook to create a state variable count with an initial value of 0. We also get a function setCount that we can use to update the count state. When the button is clicked, the increment function is called, which updates the count state by incrementing it by 1.

The return statement contains the JSX code that will be rendered to the screen. We display the current value of the count state variable and a button that calls the increment function when clicked. As you can see you can use the onClick attribute to call a function when the button is clicked by simply passing the function name to it.

So let’s create a new folder named counter with a file page.tsx in the app folder, and then paste the code above into it:

πŸ“ app
    πŸ“ api
        πŸ“ blockbuster
            πŸ“„ route.tsx
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“ counter   ✨ New folder
        πŸ“„ page.tsx  ✨ New file
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx      
    πŸ“„ page.tsx

We didn’t add this to the site menu (this page is not very exciting!), but you can access this by going to http://localhost:3000/counter in your browser. When you go to this address you should see the following error:

What is going on here? Well, in Next.js, components try to run on the server side as much as possible by default. This useState hook is a client-side thing to keep state inside the user’s browser though, so we need to tell Next.js that this component should run on the client side instead.

All you need to do is add the very first line inside the counter/page.tsx file:

'use client';
import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

Don’t worry too much about the details, we will use this in action very soon. As long as you understand the general concept and the split between having a count variable and a setCount function to update it, you’re good to go.

If you want to play around with this a bit, try to add another button to decrease the state variable again, and a third button to reset the state variable back to 0 to give you some practice.

So basically React state is like having a variable on the client side, except that when you change it, React will automatically update the page the user sees to reflect the new state, like our simple counter example.

useEffect

Now that we know how to use state in a functional component, let’s move on to the next concept: useEffect.

useEffect is another hook provided by React that allows us to perform side effects in functional components. Side effects are actions that happen outside the normal flow of the component, such as fetching data. We don’t want the whole page to have to wait for the data to arrive, so we can use useEffect to fetch the data in the background and then update the component when the data arrives.

The useEffect hook takes two arguments: a function that performs the side effect, and an optional array of dependencies (we’ll go over what this means in a second). The function passed to useEffect will be called after every render of the component. If the array of dependencies is provided, the function will only be called when one of the dependencies changes.

Here’s an example of how to use useEffect to fetch data in a functional component. This example will be conceptual for now, so you don’t have to copy this code anywhere, just focus on understanding the concept:

import React, { useState, useEffect } from 'react';

const DataFetcher = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      setData(data);
    };

    fetchData();
  }, []);

  return (
    <div>
      {data ? (
        <p>Data: {data}</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};

export default DataFetcher;

In this example, it first defines a state variable data and a function setData to update the state. This is to hold the data that will be returned from the API. (Remember, using state means that React knows and will update the page for us when the data changes).

We use the useEffect hook to fetch data from an API when the component is rendered. We’re not using a real API here as we’ve seen this part before, so this is just a conceptual example, we’ll look at a real one soon.

We define an async function fetchData that fetches data from an API endpoint and updates the component state with the fetched data using setData. Still inside of the useEffect hook, we call the fetchData function just defined above.

The empty array [] as the second argument just before useEffect closes tells React that the effect does not depend on any props or state, so it should only run once after the initial render.

The part in the return clause might look confusing but is basically just the following:

{data ? (if_data_show_this) : (else_show_loading_text)}

So don’t let the multiple line breaks fool you, it’s actually a very simple statement!

Here is a slightly different example that uses a dependency inside the dependency array (the second argument to useEffect):

import React, { useState, useEffect } from 'react';

const DataFetcher = ({ url }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const data = await response.json();
      setData(data);
    };

    fetchData();
  }, [url]);

  return (
    <div>
      {data ? (
        <p>Data: {data}</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};

export default DataFetcher;

In this example, we pass a url prop to the DataFetcher component (DataFetcher = ({ url }) =>), which is used as a dependency in the useEffect hook (note the [url] dependency array in the ‘tail’ of the useEffect call). This means that the effect will run whenever the url prop changes. This allows us to fetch data from different URLs based on the prop passed to the component.

Both of these are examples just to get an idea of how useState and useEffect work. If you’re a bit confused, feel free to go back and go through the examples again, this is tricky stuff at first and can take a moment to fully wrap your brain around.

When you have a rough idea of the concepts covered here, let’s move on to part 4 and apply these concepts to fetch our t-rex todos from the API route we created earlier, in preparation for creating something much cooler. I’ll see you there!

Leave a Comment