WS API GraphQL - fidransky/kiv-pia-labs GitHub Wiki

TOTD:

  • learn about REST API and GraphQL API differences and use cases
  • create API specs as GraphQL Schema
  • implement GraphQL API using API-first approach

Terms and theory

As we've touched on in the last lab, WS API is not just REST. One of the most acclaimed WS API frameworks in the recent years is called GraphQL.

GraphQL

Similar to REST, GraphQL is not a piece of code but rather a query language for your API, and a server-side runtime for executing queries using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.

A GraphQL service is created by defining types and fields on those types, then providing functions for each field on each type.

With GraphQL, you model your business domain as a graph

While REST API model is built on top of HTTP methods, headers etc., GraphQL takes a different approach and doesn't use HTTP basics at all:

  • Instead of having multiple URIs designed around resources, GraphQL uses a single endpoint (usually /graphql) for everything.
  • Instead of taking advantage of HTTP verbs for different purposes, GraphQL uses POST for everything, making (not only) caching difficult.
  • Instead of making use of HTTP status codes to report request results, GraphQL responds with 200 OK at all times, adding eventual error description to response body - therefore, you always have to read and parse the response body.

Even though GraphQL makes some things unnecesarilly more difficult, it comes in handy in certain situations:

diagram showing three centered circles representing usages in which GraphQL is the best fit

In GraphQL, there are always three operations:

  1. Query - The most frequently used operation, serves all read requests. It corresponds GET HTTP method in REST.
  2. Mutation - Operation type backing create, update and delete requests. Resembles POST, PUT PATCH and DELETE methods used in REST.
  3. Subscription - Allows subscribing to a stream of events. As new data arrives, a GraphQL query is applied over that data and the results are passed on. Can be implemented using any streaming protocol: SSE, WebSocket, RSocket etc.

Tools

As with REST, there's many tools to make GraphQL API development easier:

  • GraphQL Java Generator - Code generator that allows to quickly develop GraphQL clients and GraphQL servers in Java, based on a GraphQL schema.
  • GraphiQL - A graphical interactive in-browser GraphQL IDE.
  • SpectaQL - A Node.js library that generates static documentation for a GraphQL schema

Practice

In today's lab, we're going to design and implement the same WS API we did in the previous lab focused on REST APIs - this time as GraphQL API.

Once again, the API needs to support use cases and features of the semester project web app.

1. Create GraphQL schema

Use text editor of your choice to create GraphQL schema as src/main/resources/graphql/api.graphqls file. To start, read description of Semester Project to decide which types, query, mutations etc. are needed. Check GraphQL documentation to learn how to create GraphQL schema.

2. Add Spring for GraphQL

Spring Boot provides first-class support for building GraphQL APIs. We're going to use Spring for GraphQL starter here, backed by GraphQL Java library.

Add Spring for GraphQL starter to the dependencies section of your pom.xml file:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

Enable GraphiQL console by setting spring.graphql.graphiql.enabled property to true in application.properties file.

3. Configure GraphQL Java Generator Maven plugin

GraphQL Java Generator project provides a Maven plugin generating code based on GraphQL Java library. The plugin may be used to generate full server or client code but we're only going to use it to generate POJOs.

Next, add GraphQL Java common runtime dependency - a shared library used in generated code for both server and client:

<dependency>
	<groupId>com.graphql-java-generator</groupId>
	<artifactId>graphql-java-common-runtime</artifactId>
	<version>2.8</version>
</dependency>

Finally, configure the Maven plugin to the build > plugins section:

<plugin>
	<groupId>com.graphql-java-generator</groupId>
	<artifactId>graphql-maven-plugin</artifactId>
	<version>2.8</version>
	<configuration>
		<mode>server</mode>
		<packageName>cz.zcu.kiv.pia.labs.graphql</packageName>
		<schemaFileFolder>${project.basedir}/src/main/resources/graphql</schemaFileFolder>
	</configuration>
	<executions>
		<execution>
			<phase>generate-sources</phase>
			<goals>
				<goal>generatePojo</goal>
			</goals>
		</execution>
	</executions>
</plugin>

5. Configure custom scalars

By default, GraphQL Java doesn't understand our custom DateTime scalar. Add graphql-java-extended-scalars dependency:

<dependency>
	<groupId>com.graphql-java</groupId>
	<artifactId>graphql-java-extended-scalars</artifactId>
	<version>21.0</version>
</dependency>

Next, configure the Maven plugin to use the dependency. Add customScalars to the plugin's configuration section:

<customScalars>
	<customScalar>
		<graphQLTypeName>DateTime</graphQLTypeName>
		<javaType>java.time.OffsetDateTime</javaType>
		<graphQLScalarTypeStaticField>graphql.scalars.ExtendedScalars.DateTime</graphQLScalarTypeStaticField>
	</customScalar>
</customScalars>

Finally, make Spring for GraphQL aware of the DateTime scalar by defining a new bean in GraphQLConfiguration:

@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
	return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.DateTime);
}

Now, our custom DateTime scalar is mapped to Java java.time.OffsetDateTime objects.

6. Generate POJOs

With the plugin configured, let's run it to generate some POJOs (plain old Java objects). The plugin is configured to execute in generate-sources phase of Maven lifecycle. Run mvn compile to execute it.

Then, open target/generated-sources/graphql-maven-plugin/ folder to see what it generated.

7. Implement the operations

We used the plugin to only generate POJOs, leaving the implementation of GraphQL operations to us completely.

To start with the implementation, create a new GraphQLController class annotated with @Controller in cz.zcu.kiv.pia.labs.controller package:

package cz.zcu.kiv.pia.labs.controller;

import org.springframework.stereotype.Controller;

@Controller
public class GraphQLController {
}

With Spring for GraphQL, GraphQL operations are implemented as controller methods annotated with one of SchemaMapping meta-annotations:

  • @QueryMapping for GraphQL queries
  • @MutationMapping for mutations
  • @SubscriptionMapping for subscriptions

Operation names need to match their respective name in the schema (but they can be also set using the name property on the annotation). Operation attributes are mapped to method parameters annotated with @Argument. Just like with operations, attribute names must match the name in the schema (but again, can be set using the name property).

7.1 Implement retrieveDamage query

As an example, let's implement the retrieveDamage query now. Add a new retrieveDamage method to the GraphQLController class and annotate the method with @QueryMapping:

@QueryMapping
public List<DamageDTO> retrieveDamage() {
	throw new UnsupportedOperationException();
}

Next, implement the method body. Use dependency injection to autowire DamageService to the controller. Then, call DamageService#retrieveReportedDamage method to retrieve all reported damage:

return damageService.retrieveReportedDamage();

As expected, this doesn't work: the service method returns a collection of domain objects while we need to return the generated GraphQL POJO. Map the domain objects to DTOs using any method you fancy (such as Spring ConversionService or MapStruct):

return damageService.retrieveReportedDamage().stream()
		.map(damage -> conversionService.convert(damage, DamageDTO.class))
		.toList();

7.2. Tackle the (grand)children loading problem

Additionally, we might want to extend the query so that it would be able to return users related to each reported damage as well. That's a common requirement: you query GraphQL API resources together with their children, grandchildren etc. However, it is not always the case - sometimes, fetching only the resource without all the (grand)children is sufficient and therefore, it doesn't make sense to load all the (grand)children at all times (so-called overfetching). With relational databases, you need to be especially careful here: You don't want to run into N+1 queries problem but you don't want to overfetch either.

Using Spring for GraphQL, methods annotated with @BatchMapping (note its typeName and value properties) are only called when the (grand)children are actually queried so they can be used to load all (grand)children at once and only when they are actually needed.

7.3 Call the query using GraphiQL

Open http://localhost:8080/graphiql in your browser, browse through generated documentation and try to run the implemented query.

Sources

⚠️ **GitHub.com Fallback** ⚠️