DEV Community

Dustin Myers
Dustin Myers

Posted on • Updated on

Using The Request Body With Serverless Functions

Harnessing the request body within serverless functions really expands what we can do with our apps.

https://images.unsplash.com/photo-1431440869543-efaf3388c585?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb

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:

  1. Avoid any CORS issues
  2. 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) => {};
Enter fullscreen mode Exit fullscreen mode

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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! 😁🎉🔥

https://media.giphy.com/media/9K2nFglCAQClO/giphy.gif

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"
];
Enter fullscreen mode Exit fullscreen mode

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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.

https://media.giphy.com/media/QBd2kLB5qDmysEXre9/giphy.gif

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 });
  }
};

Enter fullscreen mode Exit fullscreen mode
// [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;

Enter fullscreen mode Exit fullscreen mode

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)