Learn how to develop efficient and simplified applications using GraphQL

Siri
Pictet Technologies Blog
11 min readNov 24, 2021

Compared to REST, GraphQL reduces the time to fetch data from the back end and enhances the user experience.

Illustration by Luke H

The objective of this article is to showcase the benefits of GraphQL while building a web application using it. We will develop a Travel agency web application with Angular, Spring Boot, GraphQL, and RDBMS and compare it with REST-based API development.

In the first section, we will present the business requirements of the Travel agency application. In the second section, we will set up the RDBMS database for the Travel agency application and model the data as per the business requirements. In the third section, we will build the GraphQL API for CRUD operations using Spring Boot with GraphQL-specific packages. In this section, we will also develop a similar API using REST to compare the implementations in REST and GraphQL. In the fourth section, we will use the Angular framework with the Apollo GraphQL client to render the data on the UI. The source code of the application can be found on GitHub. As per the official documentation of GraphQL:

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

1. Business requirements

We will develop a Travel agency application which helps clients to manage operations related to flight booking for trips. A trip is an entity that is booked by a client and consists of multiple flight reservations. The requirements for the users of the software are listed below.

Users must be able to:

  • Create a trip with a name and an associated client
  • Fetch a trip's details using the trip identifier
  • Edit a trip's details using the trip identifier
  • Delete a trip using the trip identifier

2. Database configuration

2.1. Set up a RDBMS database

The web application should work with any proprietary RDBMS, but we will use an H2 in-memory database to store our data. We will need the following parameters defined in the yml file.

2.2. Data model

The figure below depicts the data model and the relationships between the database entities for the Travel agency application. The Client, Flight, and FlightBooking entities are similarly created with the fields as shown in the database relationships screenshot below.

Database relationships

The Trip entity contains the following fields — id, name, client, and flightBookings.

3. GraphQL CRUD operations

The implementation of CRUD operations in GraphQL using Java requires us to know about types, data classes, and resolvers. Types are defined in a schema. GraphQL schema is defined with a language-agnostic Schema Definition Language in a file with a graphqls extension. The types defined in the schema are the most basic objects which we can retrieve from our service. A type is defined with one or more fields.

Every field returns data of the specified type. The return type of a field can be a scalar, object, enum, union, or interface (described in detail here). A field is marked as mandatory by suffixing the ! to the type. Two special types exist in GraphQL: query and mutation. Queries define the read entry points whereas mutations define the write entry points. We will define mutations to make any changes to the objects and define queries to retrieve the types. We will delve into the types, data classes, and resolvers when we implement each of the CRUD operations. The official documentation of the Java implementation is stated below:

To maintain strong typing and intuitive design, it is common to represent GraphQL types with equivalent Java classes, and fields with methods. graphql-java-tools defines two types of classes: data classes, which model the domain and are usually simple POJOs, and resolvers, which model the queries and mutations and contain the resolver functions. Often, both are needed to model a single GraphQL type.

We will need to add the graphql-spring-boot-starter dependency to the pom.xml file to convert the Spring Boot application into a GraphQL server. We will also add playground-spring-boot-starter dependency which provides the GraphQL playground that can be used for debugging and schema introspection.

3.1. Create a trip

A mutation in GraphQL is used to change the state of an entity. A mutation resolver models the change of an entity. The createTrip mutation, which is defined below, accepts an input type called TripInput. An input type is similar to an object type except it is defined with an input keyword. The TripInput defines the fields to be supplied while invoking this mutation. The TripInput in turn uses ClientInput, FlightBookingInput, and FlightInput which are defined below. The mutation and input types are defined in the graphqls file.

We will create a Trip by populating the required Client and name fields. The other fields can be set similarly. To create a Trip, we will write a method called createTrip in the TripMutation class. The TripMutation class needs to implement GraphQLMutationResolver to indicate that it is a mutation. If the clientId provided in the input does not exist in the repository, the method uses the injected clientRepository to create a client. After creating/updating the client object, we need to set this client field in the new Trip object before saving it to the database using the injected tripRepository. As seen below, we can return the newly created Trip object to the caller.

The live playground video below shows that we can invoke the createTrip mutation by specifying the fields of the schema. The input type is provided with the trip name and the client's name and age fields to be used while creating the new trip. After we click on the playground's play button, we see that the Trip entity has been created in the database and returned with the fields requested on lines 10–14. The output is seen on the right side of the playground’s play button.

In a typical REST implementation, we need to define a POST endpoint in a Controller to create a trip. As seen below, we have defined a POST endpoint at “/api/v1/trips” using a PostMapping in the TripController which expects a TripResource in the body of the HTTP request.

REST POST endpoint

A TripResource is used to accept the user-provided trip that needs to be created on the database. For the sake of simplicity, input validation, exception handling and the association of a FlightBooking to a Trip have been omitted. The TripResource is mapped to the Trip entity so that it can be saved in the database through the TripService. Finally, the URI of the newly created Trip resource is returned.

3.2. Fetch a trip

A query resolver such as the one below for Trip is used to fetch the data from the back end. The resolver needs to implement GraphQLQueryResolver. We are using the injected TripRepository to use the findById method to fetch the corresponding Trip entity. The method trip(id: ID): Trip implements the contract stated in the Query below.

TripResolver

We will define a Query type as shown below to identify the queries supported for GraphQL resolution.

Query

We will also define the Trip GraphQL object type as shown below. Each of the object types such as Client and FlightBooking used within this Trip type is defined similarly.

A query specified by the trip provides an id of the trip to be fetched. The fields on lines 4–14 are removed depending on which fields we are interested in fetching from the GraphQL server. The Trip entity is returned to the caller which can be verified from the response on the right of the play button.

As shown in the playground, GraphQL gives you the flexibility to choose only the fields that you want to retrieve from the Trip entity. This feature enables us to save the network bandwidth used as less data is transmitted compared to the fixed response in REST.

In a typical REST implementation, to retrieve a trip, we need to add a GET API endpoint to the TripController defined with a GetMapping on “/api/v1/trips/{tripId}” as shown below. The path specifies the unique resource identifier to fetch the correct trip.

REST GET endpoint

The TripController uses the TripService which in turn uses a TripRepository to fetch the relevant trip by the given tripId. The REST implementation does not provide the flexibility to choose the fields in every entity like the GraphQL implementation does.

3.3. Edit a trip

As mentioned earlier while creating a Trip, a mutation in GraphQL is used to change the state of an entity. To update an existing Trip, we will define a new method updateTrip as defined by the mutation below. As seen in the contract below, the caller needs to provide the two required fields — the id and the name. The provided trip id will be used to update the corresponding Trip with the provided trip name.

The method updateTrip will be implemented in the TripMutation class. The method simply tries to obtain a Trip using the provided trip id. If one is found, the corresponding Trip is updated with the provided trip name and saved using the tripRepository. If a Trip with the given trip id is not found, then an error is thrown.

We can invoke the updateTrip mutation by specifying the fields. The live playground video below shows that the Trip entity has been updated with the new trip name in the database and returned.

In a typical REST implementation, to update a trip, it is recommended to add a PUT API endpoint to the TripController defined with a PutMapping on “/api/v1/trips/{tripId}” as shown below. Similar to the POST endpoint, a TripResource is expected in the body of the HTTP request. The path indicates the exact trip to be modified. The code below shows how to update an existing trip by using the PUT method. We have used tripService.updateTrip method to pass the new fields to be updated into the database for the trip identified by tripId.

REST PUT endpoint

3.4. Delete a trip

We will define another mutation deleteTrip to delete an existing Trip. The deleteTrip mutation expects the id of the Trip to be deleted as seen below.

The method deleteTrip will be implemented in the TripMutation class. If the provided trip id matches an existing Trip, then the corresponding Trip is deleted using the tripRepository. If a matching Trip with the given trip id is not found, then an error is thrown. The live playground video below the code snippet shows that the delete mutation has triggered a deletion of the trip in the database.

In a typical REST implementation, to delete a trip, we need to add a DELETE API endpoint to the TripController defined with a DeleteMapping on “/api/v1/trips/{tripId}” as shown below. The method uses tripService.deleteTrip method to delete the trip identified by the tripId in the database.

REST DELETE endpoint

In each of the CRUD operations, we could end up with an error if we supply invalid values or non-existing queries/mutations/fields explained here. The error message is wrapped in the response to the GraphQL operation that was executed.

4. Invoking GraphQL endpoints from Angular

We will use the Apollo client for Angular to get the data with GraphQL. The official documentation of Apollo states the following:

Apollo Angular is the ultra-flexible, community-driven GraphQL client for Angular, JavaScript, and native platforms. It is designed from the ground up to make it easy to build UI components that fetch data with GraphQL.

The Apollo Client can be imported by creating a basic NgModule called GraphQLModule as shown below. The GraphQLModule can then be imported into the AppModule along with HttpClientModule, BrowserModule, and AppRoutingModule so that we can build a simple UI that can display the data obtained using Apollo Client from the back end. The HttpLink service uses the uri to connect to the back-end GraphQL server. The InMemoryCache is used by the Apollo Client to save the GraphQL query responses.

To display a trip on the front end, we will create a simple Angular Component as shown below. Within the ngOnInit method of the component, we need to invoke the GraphQL query(line 39) that should be run to fetch the trip. In the current example, a tripId of 1 is used and a basic HTML div element is added to render the contents of the trip(line 14). It is possible to pass variables as seen here to the GraphQL query instead of hard-coding the tripId.

To add a trip from the front end, we will need to invoke the createTrip GraphQL mutation using the Apollo mutate method. We will add a new trip using hard-coded values as shown below. However, these values can be obtained from variables mapped to user input as we would normally do in Angular applications as seen here. The return value of the method is a Trip object identical to the query method.

We will need to add an HTML div element to display the newly created Trip. This will be identical to the div element we had added to display a Trip for the query response.

In the browser, we will see the following UI when we start the Angular server while running the back-end Spring Boot application.

Conclusion

In this article, we have learned how to develop a Travel agency web application that provides operations on Trips using GraphQL and Spring Boot on the back end and Angular on the front end. We have built the GraphQL queries and mutations while comparing similar operations in the REST paradigm. A few reasons why GraphQL can be appealing are:

  1. We have the flexibility to choose only the fields that we need in the response from the back end which ensures that a smaller payload is transferred on the network.
  2. With REST APIs, when we need to change the resource returned by an API or when we need to combine different resources in the same response, we would need to change the back-end API. This implies code redeployment. Using GraphQL would mean changing the query on the front end and we can retain the back-end API as it is.
  3. The GraphQL schema is readable and the tools for development and debugging are powerful.
  4. Both GraphQL and REST can co-exist in a back-end application and they can be used depending on the use case.

--

--