Odyssey

Intro to GraphQL with Java & DGS
beta

Course overview and setupGraphQL basicsSchema definition language (SDL)Building the schemaDatafetchersApollo Sandbox ExplorerThe listings REST APIConsuming a data sourceQuery argumentsAdding the Amenity typeResolver chainsMutationsWrapping up
13. Wrapping up
4m

Overview

In the last lesson, we finished building out our request to the REST API to create a new listing. The finish line is in sight!

In this lesson, we will:

  • Complete our mutation datafetcher method, and create some new listings

Connecting the dots in the datafetcher

Our schema is complete with all the types we need to make our mutation work, and ListingService is updated with a new method. Now we just need to hook up the last piece in the datafetcher!

Let's first make sure that our generated code accounts for these new schema types, and restart our server.

Task!

Next, we'll jump back into our ListingDataFetcher file. Remember the @DgsQuery annotation we used on our class' featuredListings method? Well, there's another we can use to mark a method as responsible for a mutation: @DgsMutation!

Let's import it at the top of the file.

datafetchers/ListingDataFetcher
import com.netflix.graphql.dgs.DgsMutation;

Now we can add this annotation, and write our new method just below it. We'll give it the same name as our Mutation type's field: createListing.

public class ListingDataFetcher {
@DgsMutation
public void createListing() {}
// other methods
}

If you're using IntelliJ as your IDE, you'll see immediately that a yellow squiggly line appears beneath the name of our method. Hovering over it, we'll see a message that encourages us to use the @InputArgument annotation, and add the input argument the schema field expects.

@DgsMutation
public void createListing(@InputArgument CreateListingInput input) {
}

We haven't defined a CreateListingInput Java class we can use as input's data type, but fortunately DGS has us covered! After updating our schema and restarting our server, we should now see a new class made just for this purpose in our generated code folder: CreateListingInput!

Tip: If you don't see the CreateListingInput generated class, try restarting your server.

We can import it from our generated folder to complete our annotation.

// other imports
import com.example.listings.generated.types.CreateListingInput;
// other annotations
@DgsMutation
public void createListing(@InputArgument CreateListingInput input) {
}

We'll also find that we can now assign an appropriate return type to our createListing method, as the generated code folder now contains an CreateListingPayload class as well.

datafetchers/ListingDataFetcher
// other imports
import com.example.listings.generated.types.CreateListingInput;
import com.example.listings.generated.types.CreateListingResponse;
// other annotations
@DgsMutation
public CreateListingResponse createListing(@InputArgument CreateListingInput input) {
}

Preparing the response body

We expect our method to return an instance of CreateListingResponse, so let's create a new object that we can set when the call succeeds or fails.

CreateListingResponse response = new CreateListingResponse();

Just after this, we'll create a new try/catch block, then return the response.

CreateListingResponse response = new CreateListingResponse();
try {
// happy path
} catch (Exception e) {
// sad path
}
return response;

Inside the try, we'll attach properties to our response if things go as expected. We want to use all of the properties that get passed into this method as input, so we'll pass input right to the ListingService class' createListingRequest method. We'll capture the newly-created listing the endpoint returns as a ListingModel type called createdListing. Then we'll set some properties on our response showing that the listing was created as expected.

try {
ListingModel createdListing = listingService.createListingRequest(input);
response.setListing(createdListing);
response.setCode(200);
response.setMessage("success");
response.setSuccess(true);
} catch (Exception e) {
// sad path
}

Handling the sad path

If our call to the endpoint fails for any reason, we'll want to have control over what we send back to the client. Inside of the catch block, we can account for any exceptions that are thrown. We'll set the exception's message, along with our other properties code and success, on the returned response object.

try {
// try block logic
} catch (Exception e) {
response.setListing(null);
response.setCode(500);
response.setMessage(e.getMessage());
response.setSuccess(false);
}
@DgsMutation
public CreateListingResponse createListing(@InputArgument CreateListingInput input) {
CreateListingResponse response = new CreateListingResponse();
try {
ListingModel createdListing = listingService.createListingRequest(input);
response.setListing(createdListing);
response.setCode(200);
response.setMessage("success");
response.setSuccess(true);
} catch (Exception e) {
response.setListing(null);
response.setCode(500);
response.setMessage(e.getMessage());
response.setSuccess(false);
}
return response;
}

There's one important point to discuss here regarding the Listing class (which DGS generated from our schema) and the ListingModel class (that we wrote to extend Listing).

We opted to extend the out-of-the-box Listing class for two reasons.

  1. We wanted to have the flexibility to define our own logic (properties and methods) on the class as needed.
  2. We were disregarding some of the properties on the JSON returned by our REST API; we needed to be able to apply the @JsonIgnoreProperties onto the class so that extra properties wouldn't cause errors. We can't apply it to the generated Listing class, because we'd lose the annotation every time the server restarted!

But you might have noticed that aside from ignoring unknown properties, our ListingModel doesn't actually have any other capabilities beyond what it inherits from Listing!

The ListingModel class
package com.example.listings.models;
import com.example.listings.generated.types.Listing;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ListingModel extends Listing {}

This can introduce a sneaky bug. ListingModel looks like Listing, however it's not one and the same. And if we try to pass ListingModel in places that expect a Listing instance, we can run into trouble.

To understand this problem, recall that our CreateListingResponse type has a listing field that returns a Listing type (not ListingModel!).

type CreateListingResponse {
# ...other fields
"The newly created listing"
listing: Listing
}

The same is true of the generated CreateListingResponse class; it expects its listing property to return an instance of the Listing class.

generated.types/CreateListingResponse
public class CreateListingResponse {
// ... other properties
/**
* The newly created listing
*/
private Listing listing;

This means our createListing datafetcher method is doing something a bit strange. Instead of giving the new CreateListingResponse instance its expected Listing, it's passing a ListingModel instead; and because all of the properties are the same (for now), this flies under the radar.

But what happens if we give our ListingModel some of its own custom logic in the future?

Let's imagine that we set a new property on ListingModel—maybe a derived hash from the listing's title and id properties.

An example ListingModel with a custom hash property
@JsonIgnoreProperties(ignoreUnknown = true)
public class ListingModel extends Listing {
public String hash;
public String getHash() {
return this.hash;
}
public void setHash() {
String titleAndId = this.getTitle() + this.getId();
this.hash = titleAndId.replaceAll("\s+", "");
}

We can access the hash property on our newly created listing until we set it as the listing property on CreateListingResponse. Now our ListingModel is considered an instance of Listing instead, so the hash property is no longer valid!

public CreateListingResponse createListing(@InputArgument CreateListingInput input) {
ListingModel createdListing = listingService.createListingRequest(input);
CreateListingResponse response = new CreateListingResponse();
// We could still access createdListing.getHash() here!
if (createdListing != null) {
response.setListing(createdListing);
// We can no longer access response.listing.getHash() here! Listing does not have a hash property
response.setCode(200);
response.setMessage("success");
response.setSuccess(true);
return response;
}
// ...sad path
}

This problem isn't too big right now because we haven't given ListingModel a bunch of special properties we might want to access or use in the future. When we set our ListingModel instance as the response's listing property, it's automatically considered a Listing instance and consequently any non-Listing properties would be ignored.

We are seeing the functionality we want, so we don't need to change our implementation; however, it's important to keep in mind the shape of data our GraphQL schema expects us to return. It's our job on the server-side to prepare and return data that meets those expectations!

Running a simple mutation

We'll restart our server to make sure all of our changes have been applied.

Task!

In Explorer, fill out the following operation:

mutation CreateListing($input: CreateListingInput!) {
createListing(input: $input) {
code
success
message
listing {
title
description
costPerNight
amenities {
name
category
}
}
}
}

And add the following to the Variables panel.

{
"input": {
"title": "Mars' top destination",
"description": "A really cool place to stay",
"costPerNight": 44.0,
"amenities": ["am-1", "am-2"],
"numOfBeds": 2
}
}

When we run the operation, we'll see that this mutation works as expected! We can clearly see the values we set for code, success, and message in our happy path—not to mention the details for the new listing we've just created!

{
"data": {
"createListing": {
"code": 200,
"listing": {
"id": "f1c3a0ad-5664-4691-a7b0-961a680297df",
"description": "A really cool place to stay",
"title": "Mars' top destination",
"amenities": [
{
"id": "am-1",
"name": "Interdimensional wifi",
"category": "Accommodation Details"
},
{
"id": "am-2",
"name": "Towel",
"category": "Accommodation Details"
}
]
}
}
}
}

Bravo, you've done it!

Journey's end

Task!

You've built a GraphQL API! You've got a working GraphQL server, jam-packed with intergalactic listings, that uses a REST API as a data source. You've written queries and mutations, and learned some common GraphQL conventions along the way. You've explored how to use GraphQL arguments, variables, and input types in your schema design. Take a moment to celebrate; that's a lot of learning!

But the journey doesn't end here! When you're ready to take your GraphQL API even further, jump into the next course in this series: Federation with Java & DGS.

Thanks for joining us in this course; we hope to see you in the next one!

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.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              GraphQL schema

              A GraphQL schema defines the structure and types of data that can be queried or mutated, serving as a contract between the server and clients.

              operation

              A single query, mutation, or subscription that clients send to a GraphQL server to request or manipulate data.

              operation

              A single query, mutation, or subscription that clients send to a GraphQL server to request or manipulate data.

              mutation

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              GraphQL server

              A server that contains a GraphQL schema and can resolve client-requested operations that are executed against that schema.

              mutations

              A GraphQL operation that modifies data on the server. It allows clients to perform create, update, or delete operations, altering the underlying data.

              arguments

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              variables

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course