7. The listings REST API
10m

Overview

It's time to jump into the we'll be using throughout this course.

In this lesson, we will:

  • Explore the listings REST API
  • Create a class that can manage our requests to different REST endpoints

Exploring real data

The data that our datafetchers (or ) retrieve can come from all kinds of places: a database, a third-party API, webhooks, and so on. These are called . The beauty of is that you can mix any number of data sources to create an API that serves the needs of your client applications and graph consumers.

For the rest of the course, we're going to be using a REST API that provides listings data. We'll access it at the following URL.

Our REST API data source
https://rt-airlock-services-listing.herokuapp.com/

Unfortunately, our is missing documentation and contains some that are no longer actively used. We'll use to make an API that's a lot more accessible and intuitive for our downstream consumers.

How is our data structured?

The next question we need to answer is how our data is structured in our REST API. This impacts how we retrieve and transform that data to match the in our schema.

Our goal is to retrieve data for featured listings, and there's an endpoint for exactly that: GET /featured-listings. Let's access the /featured-listings response by opening a new browser tab and navigating to the following URL.

The GET /featured-listings endpoint
https://rt-airlock-services-listing.herokuapp.com/featured-listings

When the page loads, we can inspect the shape of the response we get back.

This endpoint returns an array containing three objects with a number of different properties.

https://rt-airlock-services-listing.herokuapp.com/featured-listings

A screenshot of the featured listings endpoint response

Note: Seeing some messy JSON? Click the "Pretty print" checkbox at the top of the screen!

Let's compare these properties with the for the Listing type we defined in our :

type Listing {
id: ID!
title: String!
numOfBeds: Int
costPerNight: Float
closedForBookings: Boolean
}

Each object in the response array includes all of these , along with a bunch of other properties that we don't need for now—hostId, latitude, and longitude, to name a few!

The first object in the response, highlighting fields that match our schema
{
"id": "listing-1",
"title": "Cave campsite in snowy MoundiiX",
"description": "Enjoy this amazing cave campsite in snow MoundiiX, where you'll be one with the nature and wildlife in this wintery planet. All space survival amenities are available. We have complementary dehydrated wine upon your arrival. Check in between 34:00 and 72:00. The nearest village is 3AU away, so please plan accordingly. Recommended for extreme outdoor adventurers.",
"costPerNight": 120,
"hostId": "user-1",
"locationType": "CAMPSITE",
"numOfBeds": 2,
"photoThumbnail": "https://res.cloudinary.com/apollographql/image/upload/v1644350721/odyssey/federation-course2/illustrations/listings-01.png",
"isFeatured": true,
"latitude": 1023.4,
"longitude": -203.4,
"closedForBookings": false,
"amenities": [{"id": "am-2"} /* additional objects */]
},

It's okay that the response contains that we don't need. Our datafetchers—along with our generated classes—will take care of picking out the data properties that match what a asks for.

Setting up our data source

We know where our data is, and we understand how it's structured. Awesome. Now, we need a way to request everything it has to offer!

We should start by creating a file that can hold all of the logic specific to this listings service—we'll call it ListingService, and we'll store it in a new package called datasources that will sit next to datafetchers and models.

đŸ“‚ java
┣ đŸ“‚ com.example.listings
┃ ┣ đŸ“‚ datafetchers
┃ ┣ đŸ“‚ datasources
┃ ┃ ┃ ┣ đŸ“„ ListingService
┃ ┣ đŸ“‚ models

First, we'll give our class the @Component annotation so the Spring framework understands how to scan, identify, and instantiate our service.

datasources/ListingService
package com.example.listings.datasources;
import org.springframework.stereotype.Component;
@Component
public class ListingService {
}

Next, we'll give our class a LISTING_API_URL property to hold the String value of our API endpoint: https://rt-airlock-services-listing.herokuapp.com.

private static final String LISTING_API_URL = "https://rt-airlock-services-listing.herokuapp.com";

We'll use Spring's RestClient to make requests to this endpoint, and we can define some of that configuration upfront since it won't change between requests.

Import RestClient at the top of your file, and give the class a client property. Here, we'll build our RestClient instance, giving it the baseUrl we'll use for all of our requests.

package com.example.listings.datasources;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class ListingService {
private static final String LISTING_API_URL = "https://rt-airlock-services-listing.herokuapp.com";
private final RestClient client = RestClient.builder().baseUrl(LISTING_API_URL).build();
}

Note: The client lets us write and send different requests to the REST API directly without needing to repeat this code in every request.

Retrieving data from /featured-listings

Next, we can give our ListingService a method specific to retrieving featured listing data. Here's the initial syntax:

public void featuredListingsRequest() {
return client
.get()
.uri("/featured-listings")
.retrieve()
}

Note: A less verbose name for this method would be featuredListings. We've opted for the longer name here to help distinguish this method from the one we defined in ListingDataFetcher! Though they'll both come into play, they're separate and distinct methods.

So far, our client builds a get request to the /featured-listings endpoint. Next, it chains on a retrieve method to actually bring the data back.

But we're not quite done here—let's remind ourselves of the shape of the data this endpoint returns.

Response object
[
{
"id": "listing-1",
"title": "Cave campsite in snowy MoundiiX",
"description": "Enjoy this amazing cave campsite in snow MoundiiX..."
// ...additional properties
}
// ...additional objects
]

In order to turn this JSON object into a Java class we can actually work with, we can chain on the method .body(). This method takes in the name of a class that we want our JSON data to be converted to.

public void featuredListingsRequest() {
return client
.get()
.uri("/featured-listings")
.retrieve()
.body();
}

We can't use our ListingModel as the class here, since we're working with a JSON array of listings. We need to be able to access each object in the JSON response, create a ListingModel with its data, and return a Java List of all the listings put back together.

To manipulate this large JSON object, we'll use the JsonNode class from the Jackson library. This will give us an initial container to hold our data before we do some additional manipulations to each object inside of it. At the top of the file, import JsonNode.

import com.fasterxml.jackson.databind.JsonNode;

Then we can update our method to use this class, passing in JsonNode.class as the parameter to body. Instead of returning the result of this call directly, we'll also update this method to capture its results in a JsonNode instance we'll call response.

JsonNode response = client
.get()
.uri("/featured-listings")
.retrieve()
.body(JsonNode.class);

Great—now we need to map through our response so that each of its objects is instantiated as a ListingModel instead.

From JSON object to Java class

To convert JSON data into a Java class, we'll use Jackson's ObjectMapper. The ObjectMapper contains methods specifically intended to deserialize JSON into Java object (or serialize objects back into JSON!). Because response is a JSON node, we can use the new mapper we've created to map each of its objects to whatever class we specify.

Let's go ahead and import it at the top of our class file, along with a few other imports we'll use shortly.

datasources/ListingService
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.listings.models.ListingModel;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;
import java.io.IOException;

Next, we'll instantiate the ObjectMapper as a private called mapper on our ListingService class.

datasources/ListingService
public class ListingService {
// ... other properties
private final ObjectMapper mapper = new ObjectMapper();
}

Back in our featuredListingsRequest method, let's update it with the following code.

public List<ListingModel> featuredListingsRequest() {
JsonNode response = client
.get()
.uri("featured-listings")
.retrieve()
.body(JsonNode.class);
if (response != null) {
return mapper.readValue(response.traverse(), new TypeReference<List<ListingModel>>() {});
}
return null;
}

This snippet does a few things:

  1. We take our REST API response and first check that it exists.
  2. If so, we call readValue on our mapper.
  3. We pass readValue two things: the JSON node (which we can read by calling its traverse method), and the resulting object the mapper should make out of the data. In this case, we want a List of ListingModel instances.
  4. Then, we return the result. Outside of the if block, in the event that our response is null, we'll just return null.

Calling the mapper.readValue method can result in a thrown exception, so let's update our function's signature: we'll give it a return type of List<ListingModel>, and indicate that it can throw an exception.

public List<ListingModel> featuredListingsRequest() throws IOException {
// ... method body
}

Practice

Which of these are true about data sources?

Key takeaways

  • With , we can access any number of to create robust APIs that meet the needs of multiple clients.
  • Bringing a new into our API starts with assessing the shape of its responses, and determining how best to map them to our schema .

Up next

We're ready to connect our to our datafetcher method—and for some actual data!

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.