Overview
It's time to jump into the data source 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 resolvers) retrieve can come from all kinds of places: a database, a third-party API, webhooks, and so on. These are called data sources. The beauty of GraphQL 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.
https://rt-airlock-services-listing.herokuapp.com/
Unfortunately, our data source is missing documentation and contains some fields that are no longer actively used. We'll use GraphQL 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 fields 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.
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.
Note: Seeing some messy JSON? Click the "Pretty print" checkbox at the top of the screen!
Let's compare these properties with the fields for the Listing
type we defined in our GraphQL schema:
type Listing {id: ID!title: String!numOfBeds: IntcostPerNight: FloatclosedForBookings: Boolean}
Each object in the response array includes all of these fields, along with a bunch of other properties that we don't need for now—hostId
, latitude
, and longitude
, to name a few!
{"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 fields 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 query 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.
package com.example.listings.datasources;import org.springframework.stereotype.Component;@Componentpublic 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;@Componentpublic 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
variable 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.
[{"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.
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 variable called mapper
on our ListingService
class.
public class ListingService {// ... other propertiesprivate 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:
- We take our REST API
response
and first check that it exists. - If so, we call
readValue
on ourmapper
. - We pass
readValue
two things: the JSON node (which we can read by calling itstraverse
method), and the resulting object the mapper should make out of the data. In this case, we want aList
ofListingModel
instances. - Then, we return the result. Outside of the
if
block, in the event that ourresponse
isnull
, we'll just returnnull
.
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
Key takeaways
- With GraphQL, we can access any number of data sources to create robust APIs that meet the needs of multiple clients.
- Bringing a new data source into our GraphQL API starts with assessing the shape of its responses, and determining how best to map them to our schema fields.
Up next
We're ready to connect our data source to our datafetcher method—and query for some actual data!
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.