Harnessing the request body within serverless functions really expands what we can do with our apps.
Greater Power
So far we have seen the most basic setup for serverless functions - returning a set of hardcoded data. In this tutorial, we will look at what we can do with serverless functions to create a more complicated application.
We will be using the Star Wars API (SWAPI) to build a multi-page application that will display a list of Star Wars characters, let your user click on the character to open the character page. We will use serverless functions for two purposes here:
- Avoid any CORS issues
- Add character images to the data provided by SWAPI, since the data does not include images
We will need to harness the power of the serverless functions request body as built by Zeit to achieve these lofty goals. Let's get started!
Hosting On Zeit
The starting code for this tutorial is in this repo here and the deployed instance here. You will need to fork it so you can connect it to a Zeit project. Go ahead and fork it now, then clone the repository to your own machine. From there, use the now cli
(download instructions) to deploy the app to Zeit. This will create a new project on Zeit and deploy it for you.
This app is built with Zeit's Next.js template. This will allow us to open a dev environment on our own machines for testing and debugging our serverless functions, while still giving us the full Zeit workflow and continuous development environment.
After you have cloned the repo, install the dependencies with yarn
. Then fire up the app with yarn run dev
. This gives you a link you can open up in your browser. You can now use the browser for debugging the Next.js app, and the terminal for debugging your serverless functions.
Refactoring To Use Serverless Functions
Right now, the app works to display the list of characters, but it is just making the fetch request to SWAPI in the component. Take a look at /pages/index.js
.
If you are unfamiliar with data fetching in a Next.js app, check out their docs on the subject. We are following those patterns in this app.
Instead of the component calling SWAPI, we want to make a request from the app to a serverless function and have the serverless function make the request to SWAPI for us. This will allow us to achieve the two things listed above.
Let's go ahead and refactor this to use a serverless function.
File structure
/pages/api directory
To start out, add an /api
directory inside the /pages
directory. Zeit will use this directory to build and host the serverless functions in the cloud. Each file in this directory will be a single serverless function and will be the endpoint that the app can use to make HTTP requests.
get-character-list.js
Now inside /pages/api
add a new file called get-character-list.js
. Remember adding API files in the last tutorial? Just like that, we can send HTTP requests to the serverless function that will be housed in this file using the endpoint "/api/get-character-list"
.
The serverless function
Now let's build the get-character-list
function. The function will start out like this:
export default (req, res) => {};
Inside this function is where we want to fetch the data for the star wars characters. Then we will return the array of characters to the client.
I have set up a fetchCharacters
function outside of the default function. I call that from the default function and then use the res
object to return the character data.
Note that we are using "node-fetch" here to give us our wonderful fetch
syntax as this is a node function.
const fetch = require("node-fetch");
const fetchCharacters = async () => {
const res = await fetch("https://swapi.py4e.com/api/people/");
const { results } = await res.json();
return results;
};
export default async (req, res) => {
try {
const characters = await fetchCharacters();
res.status(200).json({ characters });
} catch (error) {
res.status(500).json({ error });
}
};
Inside the serverless function, let's add a couple of console.logs so you can see the function at work within your terminal.
const fetch = require("node-fetch");
const fetchCharacters = async () => {
const res = await fetch("https://swapi.py4e.com/api/people/");
const { results } = await res.json();
// ADD ONE HERE
console.log(results);
return results;
};
export default async (req, res) => {
try {
const characters = await fetchCharacters();
// ADD ONE HERE
console.log(characters)
res.status(200).json({ characters });
} catch (error) {
res.status(500).json({ error });
}
};
When you have a chance to watch those logs happen, go ahead and remove them, then move on to the next step.
Updating the Next.js app
Now that we have our serverless function in place, let's update the call that is happening in /pages/index.js
. We need to change the path we provided to useSWR
to our serverless function endpoint - "/api/get-character-list"
.
Notice though, that our serverless function is changing the object that will be sent to our app. Inside the effect hook that is setting the data to state, we need to update that as well to expect an object with a characters
property.
We're getting our data through the serverless function! 😁🎉🔥
Adding thumbnail images
The final step for our list page is to add thumbnail images to the data before our serverless function returns the characters to the app. I have collected images for you. You're welcome!
const images = [
"https://boundingintocomics.com/files/2019/05/2019.05.15-06.10-boundingintocomics-5cdc56295fdf4.png",
"https://img.cinemablend.com/filter:scale/quill/7/e/9/b/6/f/7e9b6f625b1f06b8c70fe19107bf62bc0f44b6eb.jpg?mw=600",
"https://www.sideshow.com/storage/product-images/2172/r2-d2-deluxe_star-wars_feature.jpg",
"https://s.yimg.com/ny/api/res/1.2/soTg5zMneth9YIQz0ae_cw--~A/YXBwaWQ9aGlnaGxhbmRlcjtzbT0xO3c9ODAw/https://images.fatherly.com/wp-content/uploads/2018/12/darthvader-header.jpg?q=65&enable=upscale&w=1200",
"https://www2.pictures.zimbio.com/mp/oHGHV7BhCfvl.jpg",
"https://i.ytimg.com/vi/5UW1PIplmlc/maxresdefault.jpg",
"https://pm1.narvii.com/6293/db859b249381c30a6be8f8242046105e552cd54d_00.jpg",
"https://lumiere-a.akamaihd.net/v1/images/r5-d4_main_image_7d5f078e.jpeg?region=374%2C0%2C1186%2C666&width=960",
"https://lumiere-a.akamaihd.net/v1/images/image_606ff7f7.jpeg?region=0%2C0%2C1560%2C878&width=960",
"https://s.abcnews.com/images/Entertainment/ht_alec_guinness_obi_wan_kenobi_star_wars_jc_160415_16x9_992.jpg"
];
Add this array to your serverless function file, then add a .map()
to add these images to the data before you send it back.
export default async (req, res) => {
try {
const list = await fetchCharacters().catch(console.error);
// Map over chatacters to add the thumbnail image
const characters = list.map((character, index) => ({
...character,
thumbnail: images[index]
}));
res.status(200).send({ characters });
} catch (error) {
console.log({ error });
res.status(500).json({ error });
}
};
Check out the results!
Using The Request Object
Now we will build out the character page. You may have noticed that clicking on a character card navigates you to a character page. The character page URL has a dynamic param /:id
. In the /pages/Character/[id].js
file we are using Next.js' useRouter
hook to get the id param from the URL.
We want to make a request to another serverless function which will fetch the character data for us. That function will take in the id of the character we clicked on via query parameters.
The serverless function file/endpoint
The file structure here will be the same as we've seen thus far. So go ahead and set up a file called /pages/api/get-character-by-id.js
. Add a serverless function there. Just have it return some dummy data, like { message: 'hello' }
for now. Next add the same useSWR
and fetcher
functions to [id].js
. Make a request to the new function to make sure it's working.
Once you see the request happen (you can check it in the network tab in your browser) we can build in the query param and make a request to SWAPI for the character's data.
The Query Param
The request URL from the page will add a query param for the id. Our endpoint will change to this -/api/get-character-by-id?id=${id}
. Then we can grab the id in the serverless function like this - const { id } = req.query
. Easy peasy!
Your turn
Using what you've built so far, and what we just learned about the query param, build out the HTTP request in your component to make a request with the query param. In your serverless function, grab that param from the req
object and fetch the data you need from SWAPI, adding the id to the end of the URL (e.g. for Luke Skywalker, your request URL to SWAPI should be https://swapi.py4e.com/api/people/1
). When the data returns, add the correct image to the object and return the data to your app. Finally, build out your component as a character page to display the character data.
Go ahead, get working on that. I'll wait! When you're done, scroll down to see my implementation.
Solution
Great job! Aren't serverless functions awesome! Here is how I implemented everything for this page.
// get-character-by-id.js
const fetch = require("node-fetch");
// probably should move this to a util file now and just import it :)
const images = [
"https://boundingintocomics.com/files/2019/05/2019.05.15-06.10-boundingintocomics-5cdc56295fdf4.png",
"https://img.cinemablend.com/filter:scale/quill/7/e/9/b/6/f/7e9b6f625b1f06b8c70fe19107bf62bc0f44b6eb.jpg?mw=600",
"https://www.sideshow.com/storage/product-images/2172/r2-d2-deluxe_star-wars_feature.jpg",
"https://s.yimg.com/ny/api/res/1.2/soTg5zMneth9YIQz0ae_cw--~A/YXBwaWQ9aGlnaGxhbmRlcjtzbT0xO3c9ODAw/https://images.fatherly.com/wp-content/uploads/2018/12/darthvader-header.jpg?q=65&enable=upscale&w=1200",
"https://www2.pictures.zimbio.com/mp/oHGHV7BhCfvl.jpg",
"https://i.ytimg.com/vi/5UW1PIplmlc/maxresdefault.jpg",
"https://pm1.narvii.com/6293/db859b249381c30a6be8f8242046105e552cd54d_00.jpg",
"https://lumiere-a.akamaihd.net/v1/images/r5-d4_main_image_7d5f078e.jpeg?region=374%2C0%2C1186%2C666&width=960",
"https://lumiere-a.akamaihd.net/v1/images/image_606ff7f7.jpeg?region=0%2C0%2C1560%2C878&width=960",
"https://s.abcnews.com/images/Entertainment/ht_alec_guinness_obi_wan_kenobi_star_wars_jc_160415_16x9_992.jpg"
];
const fetchCharacter = async id => {
const res = await fetch(`https://swapi.py4e.com/api/people/${id}`);
const data = await res.json();
return data;
};
export default async (req, res) => {
const { id } = req.query;
// Make sure that id is present
if (!id) {
res
.status(400)
.json({ error: "No id sent - add a query param for the id" });
}
// fetch the character data and add the image to it
try {
const character = await fetchCharacter(id).catch(console.error);
character.thumbnail = images[id - 1];
res.status(200).send({ character });
} catch (error) {
console.log({ error });
res.status(500).json({ error });
}
};
// [id].js
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import fetch from "unfetch";
import useSWR from "swr";
import styles from "./Character.module.css";
async function fetcher(path) {
const res = await fetch(path);
const json = await res.json();
return json;
}
const Character = () => {
const [character, setCharacter] = useState();
const router = useRouter();
const { id } = router.query;
// fetch data using SWR
const { data } = useSWR(`/api/get-character-by-id?id=${id}`, fetcher);
useEffect(() => {
if (data && !data.error) {
setCharacter(data.character);
}
}, [data]);
// render loading message if no data yet
if (!character) return <h3>Fetching character data...</h3>;
return (
<main className="App">
<article className={styles.characterPage}>
<img src={character.thumbnail} alt={character.name} />
<h1>{character.name}</h1>
</article>
</main>
);
};
export default Character;
There we have it! I did not add much to the character page here so that the code block would be somewhat short. But hopefully, you have built it out to display all of the character's cool data! Drop a link to your hosted site in the comments when you finish! Final code can be found in here and the final deployment here.
Top comments (0)