GraphQL is a query language for APIs that was developed by Facebook. In today’s article, you will see an example on how to implement a GraphQL API on the JVM, particularly using Kotlin language and Micronaut framework; most of the examples below are reusable on other Java/Kotlin frameworks. Then we will consider how to combine multiple GraphQL services into a single data graph to provide a unified interface for querying all of your backing data sources. This is implemented using Apollo Server and Apollo Federation. Finally, the following architecture will be obtained:
Each component of the architecture answers several questions that may arise when implementing GraphQL API. The domain model includes data about planets in the Solar System and their satellites.
Planet service
The main dependencies related to GraphQL are given below:
implementation("io.micronaut.graphql:micronaut-graphql:$micronautGraphQLVersion")
implementation("io.gqljf:graphql-java-federation:$graphqlJavaFederationVersion")
The first provides integration between GraphQL Java and Micronaut,
i.e., defines common beans such as GraphQL controller and others. GraphQL controller is just a regular controller in terms of Spring and Micronaut; it processes GET and
POST requests to /graphql
path. The second dependency is a library that adds support of Apollo Federation to applications that are using GraphQL Java.
GraphQL schema is written in Schema Definition Language (SDL) and lives in the service’s resources:
type Query {
planets: [Planet!]!
planet(id: ID!): Planet
planetByName(name: String!): Planet
}
type Mutation {
createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
}
type Subscription {
latestPlanet: Planet!
}
type Planet @key(fields: "id") {
id: ID!
name: String!
# from an astronomical point of view
type: Type!
isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
details: Details!
}
interface Details {
meanRadius: Float!
mass: BigDecimal!
}
type InhabitedPlanetDetails implements Details {
meanRadius: Float!
mass: BigDecimal!
# in billions
population: Float!
}
type UninhabitedPlanetDetails implements Details {
meanRadius: Float!
mass: BigDecimal!
}
enum Type {
TERRESTRIAL_PLANET
GAS_GIANT
ICE_GIANT
DWARF_PLANET
}
input DetailsInput {
meanRadius: Float!
mass: MassInput!
population: Float
}
input MassInput {
number: Float!
tenPower: Int!
}
scalar BigDecimal
Planet.id
field has type ID
which is one of the 5 default scalar types. GraphQL Java
adds several scalar types and provides the ability to write custom scalars. The presence of the exclamation mark after type
name means that a field cannot be null and vice versa (you may notice the similarities between Kotlin and GraphQL in their ability to define nullable types). @directive
s will be discussed later. To
learn more about GraphQL schemas and their syntax see, for example, an official guide. If you use IntelliJ IDEA, you can install JS GraphQL plugin to work
with schemas.
There are two approaches to GraphQL API development:
-
schema-first
First design the schema (and therefore the API), then implement it in code
-
code-first
Schema is generated automatically based on code
Both have their pros and cons; you can find more on the topic in this blog post. For this project (and for the article) I decided to use the schema-first way. You can find a tool for either approach on this page.
There is an option in Micronaut application’s config which enables GraphQL IDE — GraphiQL — what allows making GraphQL requests from a browser:
graphql:
graphiql:
enabled: true
Main class doesn’t contain anything unusual:
object PlanetServiceApplication {
@JvmStatic
fun main(args: Array<String>) {
Micronaut.build()
.packages("io.graphqlfederation.planetservice")
.mainClass(PlanetServiceApplication.javaClass)
.start()
}
}
GraphQL bean is defined in this way:
@Bean
@Singleton
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
val schemaInputStream = resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()
val transformedGraphQLSchema = FederatedSchemaBuilder()
.schemaInputStream(schemaInputStream)
.runtimeWiring(createRuntimeWiring())
.excludeSubscriptionsFromApolloSdl(true)
.build()
return GraphQL.newGraphQL(transformedGraphQLSchema)
.instrumentation(
ChainedInstrumentation(
listOf(
FederatedTracingInstrumentation()
// uncomment if you need to enable the instrumentations. but this may affect showing documentation in a GraphQL client
// MaxQueryComplexityInstrumentation(50),
// MaxQueryDepthInstrumentation(5)
)
)
)
.build()
}
FederatedSchemaBuilder
class makes a GraphQL application adapted to the Apollo Federation specification. If you are not
going to combine multiple GraphQL Java services into a single graph, then a configuration will be different (see this tutorial).
RuntimeWiring
object is a specification of data fetchers, type resolvers and custom scalars that are needed to wire together a functional
GraphQLSchema
; it is defined as follows:
RuntimeWiring
object (source code)private fun createRuntimeWiring(): RuntimeWiring {
val detailsTypeResolver = TypeResolver { env ->
when (val details = env.getObject() as DetailsDto) {
is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
}
}
return RuntimeWiring.newRuntimeWiring()
.type("Query") { builder ->
builder
.dataFetcher("planets", planetsDataFetcher)
.dataFetcher("planet", planetDataFetcher)
.dataFetcher("planetByName", planetByNameDataFetcher)
}
.type("Mutation") { builder ->
builder.dataFetcher("createPlanet", createPlanetDataFetcher)
}
.type("Subscription") { builder ->
builder.dataFetcher("latestPlanet", latestPlanetDataFetcher)
}
.type("Planet") { builder ->
builder.dataFetcher("details", detailsDataFetcher)
}
.type("Details") { builder ->
builder.typeResolver(detailsTypeResolver)
}
.type("Type") { builder ->
builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
}
.build()
}
For the root type Query
(other root types are Mutation
and Subscription
), for instance, planets
field is defined in the schema, therefore it is needed to provide a DataFetcher
for it:
PlanetsDataFetcher
(source code)@Singleton
class PlanetsDataFetcher(
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<List<PlanetDto>> {
override fun get(env: DataFetchingEnvironment): List<PlanetDto> = planetService.getAll()
.map { planetConverter.toDto(it) }
}
Here the env
input parameter contains all the context that is needed to fetch a value. The method just gets all the items from a repository and converts them into DTO. Conversion is performed in this way:
PlanetConverter
(source code)@Singleton
class PlanetConverter : GenericConverter<Planet, PlanetDto> {
override fun toDto(entity: Planet): PlanetDto {
val details = DetailsDto(id = entity.detailsId)
return PlanetDto(
id = entity.id,
name = entity.name,
type = entity.type,
details = details
)
}
}
GenericConverter
is just a common interface for Entity → DTO
transformation. Let’s suppose details
is a heavy field, then we should return it only if it was requested. So in the
snippet above only simple properties are converted and for details
object only id
field is filled. Earlier, in the definition of the RuntimeWiring
object, DataFetcher
for details
field of Planet
type was specified; it is defined as follows (it needs to know a value of details.id
field):
DetailsDataFetcher
(source code)@Singleton
class DetailsDataFetcher : DataFetcher<CompletableFuture<DetailsDto>> {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun get(env: DataFetchingEnvironment): CompletableFuture<DetailsDto> {
val planetDto = env.getSource<PlanetDto>()
log.info("Resolve `details` field for planet: ${planetDto.name}")
val dataLoader: DataLoader<Long, DetailsDto> = env.getDataLoader("details")
return dataLoader.load(planetDto.details.id)
}
}
Here you see that it is possible to return CompletableFuture
instead of an actual object. More simple would be just to get Details
entity from DetailsService
, but this would be a
naive implementation that leads to the N+1 problem: if we would make GraphQL request say:
{
planets {
name
details {
meanRadius
}
}
}
then for each planet’s details
field separate SQL call would be made. To prevent this, java-dataloader library is used; BatchLoader
and DataLoaderRegistry
beans should be defined:
// bean's scope is `Singleton`, because `BatchLoader` is stateless
@Bean
@Singleton
fun detailsBatchLoader(): BatchLoader<Long, DetailsDto> = BatchLoader { keys ->
CompletableFuture.supplyAsync {
detailsService.getByIds(keys)
.map { detailsConverter.toDto(it) }
}
}
// bean's (default) scope is `Prototype`, because `DataLoader` is stateful
@Bean
fun dataLoaderRegistry() = DataLoaderRegistry().apply {
val detailsDataLoader = DataLoader.newDataLoader(detailsBatchLoader())
register("details", detailsDataLoader)
}
BatchLoader
makes it possible to get a bunch of Details
at once. Therefore, only two SQL calls will be performed instead of N+1 requests. You can make sure of it by making the GraphQL
request above and seeing at the application’s log where actual SQL queries will be shown. BatchLoader
is stateless, so it may be a singleton object. DataLoader
simply points to the
BatchLoader
; it is stateful, therefore, it should be created per request as well as DataLoaderRegistry
. Depending on your business requirements you may need to share data across
web requests which is also possible. More on batching and caching is in the GraphQL Java documentation.
Details
in GraphQL schema is defined as an interface, therefore at the first part of the RuntimeWiring
object’s definition TypeResolver
object is created to specify to what concrete GraphQL
type what DTO should be resolved:
TypeResolver
(source code)val detailsTypeResolver = TypeResolver { env ->
when (val details = env.getObject() as DetailsDto) {
is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
}
}
It is also needed to specify Java runtime values for values of Type
enum defined in the schema (it seems like this is necessary only for using an enum in mutations):
.type("Type") { builder ->
builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
}
After launching a service, you can navigate to http://localhost:8082/graphiql
and see GraphiQL IDE, in which it is possible to make any requests defined in the schema; the IDE is divided into three
parts: request (query/mutation/subscription), response, and documentation:
There are other GraphQL IDEs, for example, GraphQL Playground and Altair (which is available as a desktop application, browser extension, and web page). The latter I will use further:
On the documentation part, there are two additional queries besides defined in the schema: _service
and _entities
. They are created by the library that adapts the application to the
Apollo Federation specification; this question will be discussed later.
If you navigate to the Planet
type, you will see its definition:
Both the comment for type
field and the @deprecated
directive for isRotatingAroundSun
field are specified in the
schema.
There is one mutation defined in the schema:
type Mutation {
createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
}
As a query, it also allows requesting fields of a returning type. Note that if you need to pass an object as an input parameter, input
type should be used instead of queries' type
:
input DetailsInput {
meanRadius: Float!
mass: MassInput!
population: Float
}
input MassInput {
number: Float!
tenPower: Int!
}
As for a query, DataFetcher
should be defined for a mutation:
DataFetcher
for the mutation (source code)@Singleton
class CreatePlanetDataFetcher(
private val objectMapper: ObjectMapper,
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<PlanetDto> {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun get(env: DataFetchingEnvironment): PlanetDto {
log.info("Trying to create planet")
val name = env.getArgument<String>("name")
val type = env.getArgument<Planet.Type>("type")
val detailsInputDto = objectMapper.convertValue(env.getArgument("details"), DetailsInputDto::class.java)
val newPlanet = planetService.create(
name,
type,
detailsInputDto.meanRadius,
detailsInputDto.mass.number,
detailsInputDto.mass.tenPower,
detailsInputDto.population
)
return planetConverter.toDto(newPlanet)
}
}
Let’s suppose that someone wants to be notified of a planet adding event. For such a purpose subscription can be used:
type Subscription {
latestPlanet: Planet!
}
The subscription’s DataFetcher
returns Publisher
:
DataFetcher
for subscription (source code)@Singleton
class LatestPlanetDataFetcher(
private val planetService: PlanetService,
private val planetConverter: PlanetConverter
) : DataFetcher<Publisher<PlanetDto>> {
override fun get(environment: DataFetchingEnvironment) = planetService.getLatestPlanet().map { planetConverter.toDto(it) }
}
To test the mutation and the subscription open two tabs of any GraphQL IDE or two different IDEs; in the first subscribe as follows (it may be required to set
subscription URL ws://localhost:8082/graphql-ws
):
subscription {
latestPlanet {
name
type
}
}
In the second perform mutation like this:
mutation {
createPlanet(
name: "Pluto"
type: DWARF_PLANET
details: { meanRadius: 50.0, mass: { number: 0.0146, tenPower: 24 } }
) {
id
}
}
The subscribed client will be notified of a planet creation:
Subscriptions in Micronaut are enabled by using the following option:
graphql:
graphql-ws:
enabled: true
Another example of subscriptions in Micronaut is a chat application. For more information on subscriptions, see GraphQL Java documentation.
Tests for queries and mutations can be written like this:
@Test
fun testPlanets() {
val query = """
{
planets {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
}
""".trimIndent()
val response = graphQLClient.sendRequest(query, object : TypeReference<List<PlanetDto>>() {})
assertThat(response, hasSize(8))
assertThat(
response, contains(
hasProperty("name", `is`("Mercury")),
hasProperty("name", `is`("Venus")),
hasProperty("name", `is`("Earth")),
hasProperty("name", `is`("Mars")),
hasProperty("name", `is`("Jupiter")),
hasProperty("name", `is`("Saturn")),
hasProperty("name", `is`("Uranus")),
hasProperty("name", `is`("Neptune"))
)
)
}
If a part of a query can be reused in another query, you can use fragments:
private val planetFragment = """
fragment planetFragment on Planet {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
""".trimIndent()
@Test
fun testPlanetById() {
val earthId = 3
val query = """
{
planet(id: $earthId) {
... planetFragment
}
}
$planetFragment
""".trimIndent()
val response = graphQLClient.sendRequest(query, object : TypeReference<PlanetDto>() {})
// assertions
}
To use variables, you can write tests in this way:
@Test
fun testPlanetByName() {
val variables = mapOf("name" to "Earth")
val query = """
query testPlanetByName(${'$'}name: String!){
planetByName(name: ${'$'}name) {
... planetFragment
}
}
$planetFragment
""".trimIndent()
val response = graphQLClient.sendRequest(query, variables, null, object : TypeReference<PlanetDto>() {})
// assertions
}
This approach looks a little strange because in Kotlin raw strings, or string templates, you can’t escape a symbol, so to
represent $
(variable symbol in GraphQL) you need to write ${'$'}
.
Injected GraphQLClient
in the snippets above is just a
self-written class (it is framework-agnostic
by using OkHttp library). There are other Java GraphQL clients, for example, Apollo GraphQL Client for Android and the JVM, but I haven’t used them yet.
Data of all 3 services are stored in H2 in-memory databases and are accessed using Hibernate ORM provided by the micronaut-data-hibernate-jpa
library. The databases are initialized with data
at the applications' startup.
Auth service
GraphQL doesn’t provide means for authentication and authorization. For this project, I decided to use JWT. Auth service is only responsible for JWT token issue and validation and contains just one query and one mutation:
type Query {
validateToken(token: String!): Boolean!
}
type Mutation {
signIn(data: SignInData!): SignInResponse!
}
input SignInData {
username: String!
password: String!
}
type SignInResponse {
username: String!
token: String!
}
To get a JWT you need to perform in a GraphQL IDE the following mutation (Auth service URL is http://localhost:8081/graphql
):
mutation {
signIn(data: {username: "john_doe", password: "password"}) {
token
}
}
Including the Authorization
header to further requests (it is possible in Altair and GraphQL Playground IDEs) allows access to protected resources; this will be shown in the next section.
The header value should be specified as Bearer $JWT
.
Working with JWT is done using the micronaut-security-jwt
library.
Satellite service
The service’s schema looks like that:
type Query {
satellites: [Satellite!]!
satellite(id: ID!): Satellite
satelliteByName(name: String!): Satellite
}
type Satellite {
id: ID!
name: String!
lifeExists: LifeExists!
firstSpacecraftLandingDate: Date
}
type Planet @key(fields: "id") @extends {
id: ID! @external
satellites: [Satellite!]!
}
enum LifeExists {
YES,
OPEN_QUESTION,
NO_DATA
}
scalar Date
Say in the Satellite
type lifeExists
field should be protected. Many frameworks offer security approach in which you just need to specify routes and different security policies for them,
but such an approach can’t be used to protect some specific GraphQL query/mutation/subscription or types' fields, because all requests are sent to /graphql
endpoint. Only you can do is to
configure a couple of GraphQL-specific endpoints, for example, as follows (requests to any other endpoints will be disallowed):
micronaut:
security:
enabled: true
intercept-url-map:
- pattern: /graphql
httpMethod: POST
access:
- isAnonymous()
- pattern: /graphiql
httpMethod: GET
access:
- isAnonymous()
It is not recommended putting authorization logic into DataFetcher
to not make an application’s logic brittle:
LifeExistsDataFetcher
(source code)@Singleton
class LifeExistsDataFetcher(
private val satelliteService: SatelliteService
) : DataFetcher<Satellite.LifeExists> {
override fun get(env: DataFetchingEnvironment): Satellite.LifeExists {
val id = env.getSource<SatelliteDto>().id
return satelliteService.getLifeExists(id)
}
}
Protection of a field can be done using a framework’s means and custom logic:
SatelliteService
(source code)@Singleton
class SatelliteService(
private val repository: SatelliteRepository,
private val securityService: SecurityService
) {
// other stuff
fun getLifeExists(id: Long): Satellite.LifeExists {
val userIsAuthenticated = securityService.isAuthenticated
if (userIsAuthenticated) {
return repository.findById(id)
.orElseThrow { RuntimeException("Can't find satellite by id=$id") }
.lifeExists
} else {
throw RuntimeException("`lifeExists` property can only be accessed by authenticated users")
}
}
}
The following request can only be successful if you will specify the Authorization
header with received JWT (see the previous section):
{
satellite(id: "1") {
name
lifeExists
}
}
The service validates token automatically using the framework. The secret is stored in the configuration file (in the Base64 form):
micronaut:
security:
token:
jwt:
enabled: true
signatures:
secret:
validation:
base64: true
# In real life, the secret should NOT be under source control (instead of it, for example, in environment variable).
# It is here just for simplicity.
secret: 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA=='
jws-algorithm: HS256
In real life, the secret can be stored in an environment variable to share it with several services. Also, instead of the sharing validation of the JWT can be used (validateToken
method was shown in
the previous section).
Such scalar types as Date
, DateTime
, and some others can be added to GraphQL Java service using graphql-java-extended-scalars library
(com.graphql-java:graphql-java-extended-scalars:$graphqlJavaExtendedScalarsVersion
in build script). Then the required types should be declared in the schema (scalar Date
) and registered:
private fun createRuntimeWiring(): RuntimeWiring = RuntimeWiring.newRuntimeWiring()
// other stuff
.scalar(ExtendedScalars.Date)
.build()
Then they can be used as others:
{
satelliteByName(name: "Moon") {
firstSpacecraftLandingDate
}
}
{
"data": {
"satelliteByName": {
"firstSpacecraftLandingDate": "1959-09-13"
}
}
}
There are different security threats to your GraphQL API (see this checklist to learn more). For example, if the domain model of the described project was a bit more complex, the following request would be possible:
{
planet(id: "1") {
star {
planets {
star {
planets {
star {
... # more deep nesting!
}
}
}
}
}
}
}
To make such a request invalid MaxQueryDepthInstrumentation
should be used. To restrict query complexity MaxQueryComplexityInstrumentation
can be specified; it optionally takes
FieldComplexityCalculator
in which it is possible to define fine-grained calculation criteria. The next code snippet shows an example on how to apply multiple instrumentations
(FieldComplexityCalculator
there calculates complexity like default one based on the assumption that every field’s cost is 1):
return GraphQL.newGraphQL(transformedGraphQLSchema)
// other stuff
.instrumentation(
ChainedInstrumentation(
listOf(
FederatedTracingInstrumentation(),
MaxQueryComplexityInstrumentation(50, FieldComplexityCalculator { env, child ->
1 + child
}),
MaxQueryDepthInstrumentation(5)
)
)
)
.build()
Note that if you specify MaxQueryDepthInstrumentation
and/or MaxQueryComplexityInstrumentation
then documentation of a service may stop showing in your GraphQL IDE. This is because the IDE
tries to perform IntrospectionQuery
which has considerable depth and complexity (discussion on this is on GitHub). FederatedTracingInstrumentation
is
used to make your server generate performance traces and return them along with responses to Apollo Gateway (which then can send them to
Apollo Graph Manager; it seems like a subscription is needed to use this function). More on instrumentation see in
GraphQL Java documentation.
There is an ability to customize requests; it differs in different frameworks. In Micronaut, for example, it is done in this way:
GraphQLExecutionInputCustomizer
(source code)@Singleton
// mark it as primary to override the default one
@Primary
class HeaderValueProviderGraphQLExecutionInputCustomizer : DefaultGraphQLExecutionInputCustomizer() {
override fun customize(executionInput: ExecutionInput, httpRequest: HttpRequest<*>): Publisher<ExecutionInput> {
val context = HTTPRequestHeaders { headerName ->
httpRequest.headers[headerName]
}
return Publishers.just(executionInput.transform {
it.context(context)
})
}
}
This customizer provides an ability to FederatedTracingInstrumentation
to see headers to check whether a request have come from Apollo Server and therefore whether to return performance traces.
To have an ability to handle all exceptions during data fetching in one place and to define custom exception handling logic you need to provide a bean as follows:
@Singleton
class CustomDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() {
private val log = LoggerFactory.getLogger(this.javaClass)
override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult {
val exception = handlerParameters.exception
log.error("Exception while GraphQL data fetching", exception)
val error = object : GraphQLError {
override fun getMessage(): String = "There was an error: ${exception.message}"
override fun getErrorType(): ErrorType? = null
override fun getLocations(): MutableList<SourceLocation>? = null
}
return DataFetcherExceptionHandlerResult.newResult().error(error).build()
}
}
The main purpose of the service is to demonstrate how the distributed GraphQL entity (Planet
) can be resolved in two (or more) services and then accessed through Apollo Server. Planet
type was earlier
defined in the Planet service in this way:
Planet
type in Planet service (source code)type Planet @key(fields: "id") {
id: ID!
name: String!
# from an astronomical point of view
type: Type!
isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
details: Details!
}
Satellite service adds the satellites
field (which contains only non-nullable elements and is non-nullable by itself as follows from its declaration) to the Planet
entity:
Planet
type in Satellite service (source code)type Satellite {
id: ID!
name: String!
lifeExists: LifeExists!
firstSpacecraftLandingDate: Date
}
type Planet @key(fields: "id") @extends {
id: ID! @external
satellites: [Satellite!]!
}
In Apollo Federation terms Planet
is an entity — a type that can be referenced by another service (by Satellite service in this case which defines a stub for Planet
type). Declaring an entity is done by adding a @key
directive
to the type definition. This directive tells other services which fields to use to uniquely identify a particular instance of the type. The @extends
annotation declares that Planet
is an
entity defined elsewhere (in Planet service in this case). More on Apollo Federation core concepts see in Apollo documentation.
There are two libraries for supporting Apollo Federation; both are built on top of GraphQL Java but didn’t fit the project:
-
This is a set of libraries written in Kotlin; it uses the code-first approach without a necessity to define a schema. The project contains
graphql-kotlin-federation
module, but it seems like you need to use this library in conjunction with other libraries of the project. -
The project’s development is not very active and the API could be improved.
So I decided to refactor the second library to enhance the API and make it more convenient. The project is on GitHub.
To specify how a particular instance of the Planet
entity should be fetched FederatedEntityResolver
object is defined (basically, it points what should be filled in the Planet.satellites
field);
then the resolver is passed to FederatedSchemaBuilder
:
@Bean
@Singleton
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
// other stuff
val planetEntityResolver = object : FederatedEntityResolver<Long, PlanetDto>("Planet", { id ->
log.info("`Planet` entity with id=$id was requested")
val satellites = satelliteService.getByPlanetId(id)
PlanetDto(id = id, satellites = satellites.map { satelliteConverter.toDto(it) })
}) {}
val transformedGraphQLSchema = FederatedSchemaBuilder()
.schemaInputStream(schemaInputStream)
.runtimeWiring(createRuntimeWiring())
.federatedEntitiesResolvers(listOf(planetEntityResolver))
.build()
// other stuff
}
The library generates two additional queries (_service
and _entities
) that will be used by Apollo Server. These queries are internal, i.e., they won’t be exposed by Apollo Server. A
service with Apollo Federation support still can work independently. The library’s API may change in the future.
Apollo Server
Apollo Server and Apollo Federation allow achieving 2 main goals:
-
create a single endpoint for GraphQL APIs' clients
-
create a single data graph from distributed entities
That is even if you don’t use federated entities, it is more convenient for frontend developers to use a single endpoint than multiple endpoints.
There is another way for creating single GraphQL schema — schema stitching — but now on the Apollo site, it is marked as deprecated. However, there is a library that implements this approach: Nadel. It is written by creators of GraphQL Java and has nothing to do with Apollo Federation; I haven’t used it yet.
This module includes the following sources:
{
"name": "api-gateway",
"main": "gateway.js",
"scripts": {
"start-gateway": "nodemon gateway.js"
},
"devDependencies": {
"concurrently": "5.1.0",
"nodemon": "2.0.2"
},
"dependencies": {
"@apollo/gateway": "0.12.0",
"apollo-server": "2.10.0",
"graphql": "14.6.0"
}
}
const {ApolloServer} = require("apollo-server");
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({request, context}) {
request.http.headers.set('Authorization', context.authHeaderValue);
}
}
const gateway = new ApolloGateway({
serviceList: [
{name: "auth-service", url: "http://localhost:8081/graphql"},
{name: "planet-service", url: "http://localhost:8082/graphql"},
{name: "satellite-service", url: "http://localhost:8083/graphql"}
],
buildService({name, url}) {
return new AuthenticatedDataSource({url});
},
});
const server = new ApolloServer({
gateway, subscriptions: false, context: ({req}) => ({
authHeaderValue: req.headers.authorization
})
});
server.listen().then(({url}) => {
console.log(`🚀 Server ready at ${url}`);
});
Maybe the source above can be simplified (especially in the part of passing authorization header); if so, feel free to contact me for change.
Authentication still works as was described earlier (you just need to specify Authorization
header and its value). Also, it is possible to change security implementation, for example,
move JWT validation logic from downstream services to the apollo-server
module.
To launch this service you need to make sure you’ve launched 3 GraphQL Java services described previously, cd
to the apollo-server
directory, and run the following:
-
npm install
-
npm run start-gateway
A successful launch should look like this:
[nodemon] 2.0.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node gateway.js`
� Server ready at http://localhost:4000/
[INFO] Sat Feb 15 2020 13:22:37 GMT+0300 (Moscow Standard Time) apollo-gateway: Gateway successfully loaded schema.
* Mode: unmanaged
Then you can use a unified interface to perform GraphQL requests to all of your services:
Also, you can navigate to http://localhost:4000/playground
in your browser and use a built-in Playground IDE.
Note that now even if you have set limitations on queries using MaxQueryComplexityInstrumentation
and/or MaxQueryDepthInstrumentation
with reasonable parameters as was described above,
GraphQL IDE does show the combined documentation. This is because Apollo Server is getting each service’s schema by performing simple { _service { sdl } }
query instead of sizeable
IntrospectionQuery
.
Currently, there are some limitations of such an architecture which I encountered while implementing this project:
-
subscriptions are not supported by Apollo Gateway (but still works in a standalone GraphQL Java service)
That’s why in Planet service
.excludeSubscriptionsFromApolloSdl(true)
was specified. -
a service trying to extend GraphQL interface requires knowledge of concrete implementations
An application written in any language or framework can be added as a downstream service of Apollo Server if it implements Federation specification; a list of libraries that offer such support is available on Apollo documentation.
Conclusion
In this article, I tried to summarize my experience with GraphQL on the JVM. Also, I showed how to combine APIs of GraphQL Java services to provide a unified GraphQL interface; in such an architecture an entity can be distributed among several microservices. It is achieved by using Apollo Server, Apollo Federation, and graphql-java-federation library. The source code of the considered project is on GitHub. Thanks for reading!