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
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.
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:
In GraphQL, there are always three operations:
- Query - The most frequently used operation, serves all read requests. It corresponds
GET
HTTP method in REST. - Mutation - Operation type backing create, update and delete requests. Resembles
POST
,PUT
PATCH
andDELETE
methods used in REST. - 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.
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
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.
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.
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.
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>
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.
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.
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).
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();
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.
Open http://localhost:8080/graphiql in your browser, browse through generated documentation and try to run the implemented query.