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 thetodo_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 functiontodo => ()
to define the function. It’s a bit like Python and sayingfor 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 theid
field from thetodo
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!