Learn how to develop efficient and simplified applications using GraphQL
Compared to REST, GraphQL reduces the time to fetch data from the back end and enhances the user experience.
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.
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.
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.
We will define a Query
type as shown below to identify the queries supported for GraphQL resolution.
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.
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
.
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.
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:
- 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.
- 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.
- The GraphQL schema is readable and the tools for development and debugging are powerful.
- Both GraphQL and REST can co-exist in a back-end application and they can be used depending on the use case.