DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • How To Build Web Service Using Spring Boot 2.x
  • The First Annual Recap From JPA Buddy
  • Why and When to Use GraphQL
  • How to Create Microservices Using http4k

Trending

  • Data Lake vs. Warehouse vs. Lakehouse vs. Mart: Choosing the Right Architecture for Your Business
  • Designing AI Multi-Agent Systems in Java
  • Yet Another GenAI Nightmare: Seven Shadow AI Pitfalls to Avoid
  • How Kubernetes Cluster Sizing Affects Performance and Cost Efficiency in Cloud Deployments
  1. DZone
  2. Data Engineering
  3. Databases
  4. Create a Single Microservices Endpoint With GraphQL, Kotlin, and Micronaut

Create a Single Microservices Endpoint With GraphQL, Kotlin, and Micronaut

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.

By 
Roman Kudryashov user avatar
Roman Kudryashov
DZone Core CORE ·
Mar. 16, 20 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
16.9K Views

Join the DZone community and get the full member experience.

Join For Free

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:

architecture

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.

Prerequisites

  • JDK 11+
  • npm

You might also like: Microservices in Practice: From Architecture to Deployment

Planet Service

The main dependencies related to GraphQL are given below:

Kotlin
x
 
1
implementation("io.micronaut.graphql:micronaut-graphql:$micronautGraphQLVersion")
2
implementation("io.gqljf:graphql-java-federation:$graphqlJavaFederationVersion")

GraphQL dependencies (source code)

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:

Java
xxxxxxxxxx
1
59
 
1
type Query {
2
    planets: [Planet!]!
3
    planet(id: ID!): Planet
4
    planetByName(name: String!): Planet
5
}
6
7
type Mutation {
8
    createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
9
}
10
11
type Subscription {
12
    latestPlanet: Planet!
13
}
14
15
type Planet @key(fields: "id") {
16
    id: ID!
17
    name: String!
18
    # from an astronomical point of view
19
    type: Type!
20
    isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
21
    details: Details!
22
}
23
24
interface Details {
25
    meanRadius: Float!
26
    mass: BigDecimal!
27
}
28
29
type InhabitedPlanetDetails implements Details {
30
    meanRadius: Float!
31
    mass: BigDecimal!
32
    # in billions
33
    population: Float!
34
}
35
36
type UninhabitedPlanetDetails implements Details {
37
    meanRadius: Float!
38
    mass: BigDecimal!
39
}
40
41
enum Type {
42
    TERRESTRIAL_PLANET
43
    GAS_GIANT
44
    ICE_GIANT
45
    DWARF_PLANET
46
}
47
48
input DetailsInput {
49
    meanRadius: Float!
50
    mass: MassInput!
51
    population: Float
52
}
53
54
input MassInput {
55
    number: Float!
56
    tenPower: Int!
57
}
58
59
scalar BigDecimal

Schema of Planet service (source code)

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:

YAML
xxxxxxxxxx
1
 
1
graphql:
2
  graphiql:
3
    enabled: true

Switching on GraphiQL (source code)

Main class doesn’t contain anything unusual:

Kotlin
xxxxxxxxxx
1
10
 
1
object PlanetServiceApplication {
2
3
    @JvmStatic
4
    fun main(args: Array<String>) {
5
        Micronaut.build()
6
            .packages("io.graphqlfederation.planetservice")
7
            .mainClass(PlanetServiceApplication.javaClass)
8
            .start()
9
    }
10
}

Main class (source code)

GraphQL bean is defined in this way:

Kotlin
xxxxxxxxxx
1
23
 
1
@Bean
2
@Singleton
3
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
4
    val schemaInputStream = resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()
5
    val transformedGraphQLSchema = FederatedSchemaBuilder()
6
        .schemaInputStream(schemaInputStream)
7
        .runtimeWiring(createRuntimeWiring())
8
        .excludeSubscriptionsFromApolloSdl(true)
9
        .build()
10
11
    return GraphQL.newGraphQL(transformedGraphQLSchema)
12
        .instrumentation(
13
            ChainedInstrumentation(
14
                listOf(
15
                    FederatedTracingInstrumentation()
16
                    // uncomment if you need to enable the instrumentations. but this may affect showing documentation in a GraphQL client
17
                    // MaxQueryComplexityInstrumentation(50),
18
                    // MaxQueryDepthInstrumentation(5)
19
                )
20
            )
21
        )
22
        .build()
23
}

GraphQL configuration (source code)

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:

Kotlin
xxxxxxxxxx
1
33
 
1
private fun createRuntimeWiring(): RuntimeWiring {
2
    val detailsTypeResolver = TypeResolver { env ->
3
        when (val details = env.getObject() as DetailsDto) {
4
            is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
5
            is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
6
            else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
7
        }
8
    }
9
10
    return RuntimeWiring.newRuntimeWiring()
11
        .type("Query") { builder ->
12
            builder
13
                .dataFetcher("planets", planetsDataFetcher)
14
                .dataFetcher("planet", planetDataFetcher)
15
                .dataFetcher("planetByName", planetByNameDataFetcher)
16
        }
17
        .type("Mutation") { builder ->
18
            builder.dataFetcher("createPlanet", createPlanetDataFetcher)
19
        }
20
        .type("Subscription") { builder ->
21
            builder.dataFetcher("latestPlanet", latestPlanetDataFetcher)
22
        }
23
        .type("Planet") { builder ->
24
            builder.dataFetcher("details", detailsDataFetcher)
25
        }
26
        .type("Details") { builder ->
27
            builder.typeResolver(detailsTypeResolver)
28
        }
29
        .type("Type") { builder ->
30
            builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
31
        }
32
        .build()
33
}

Creating a RuntimeWiring object (source code)

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:

Kotlin
xxxxxxxxxx
1
 
1
@Singleton
2
class PlanetsDataFetcher(
3
    private val planetService: PlanetService,
4
    private val planetConverter: PlanetConverter
5
) : DataFetcher<List<PlanetDto>> {
6
    override fun get(env: DataFetchingEnvironment): List<PlanetDto> = planetService.getAll()
7
        .map { planetConverter.toDto(it) }
8
}

PlanetsDataFetcher (source code)

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:

Kotlin
xxxxxxxxxx
1
13
 
1
@Singleton
2
class PlanetConverter : GenericConverter<Planet, PlanetDto> {
3
    override fun toDto(entity: Planet): PlanetDto {
4
        val details = DetailsDto(id = entity.detailsId)
5
6
        return PlanetDto(
7
            id = entity.id,
8
            name = entity.name,
9
            type = entity.type,
10
            details = details
11
        )
12
    }
13
}

PlanetConverter (source code)

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):

Kotlin
xxxxxxxxxx
1
14
 
1
@Singleton
2
class DetailsDataFetcher : DataFetcher<CompletableFuture<DetailsDto>> {
3
4
    private val log = LoggerFactory.getLogger(this.javaClass)
5
6
    override fun get(env: DataFetchingEnvironment): CompletableFuture<DetailsDto> {
7
        val planetDto = env.getSource<PlanetDto>()
8
        log.info("Resolve `details` field for planet: ${planetDto.name}")
9
10
        val dataLoader: DataLoader<Long, DetailsDto> = env.getDataLoader("details")
11
12
        return dataLoader.load(planetDto.details.id)
13
    }
14
}

DetailsDataFetcher (source code)

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:

Java
 




xxxxxxxxxx
1


 
1
{
2
  planets {
3
    name
4
    details {
5
      meanRadius
6
    }
7
  }
8
}


Example of possible resource-consuming GraphQL request

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:

Kotlin
xxxxxxxxxx
1
16
 
1
// bean's scope is `Singleton`, because `BatchLoader` is stateless
2
@Bean
3
@Singleton
4
fun detailsBatchLoader(): BatchLoader<Long, DetailsDto> = BatchLoader { keys ->
5
    CompletableFuture.supplyAsync {
6
        detailsService.getByIds(keys)
7
            .map { detailsConverter.toDto(it) }
8
    }
9
}
10
11
// bean's (default) scope is `Prototype`, because `DataLoader` is stateful
12
@Bean
13
fun dataLoaderRegistry() = DataLoaderRegistry().apply {
14
    val detailsDataLoader = DataLoader.newDataLoader(detailsBatchLoader())
15
    register("details", detailsDataLoader)
16
}

BatchLoader and DataLoaderRegistry (source code)

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:

Kotlin
xxxxxxxxxx
1
 
1
val detailsTypeResolver = TypeResolver { env ->
2
    when (val details = env.getObject() as DetailsDto) {
3
        is InhabitedPlanetDetailsDto -> env.schema.getObjectType("InhabitedPlanetDetails")
4
        is UninhabitedPlanetDetailsDto -> env.schema.getObjectType("UninhabitedPlanetDetails")
5
        else -> throw RuntimeException("Unexpected details type: ${details.javaClass.name}")
6
    }
7
}

TypeResolver (source code)

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):

Kotlin
xxxxxxxxxx
1
 
1
.type("Type") { builder ->
2
    builder.enumValues(NaturalEnumValuesProvider(Planet.Type::class.java))
3
}

Enum processing (source code)

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:

graphiql

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:

altair

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:

Java
 




xxxxxxxxxx
1


 
1
type Mutation {
2
    createPlanet(name: String!, type: Type!, details: DetailsInput!): Planet!
3
}


Mutation (source code)

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:

Java
 




xxxxxxxxxx
1
10


 
1
input DetailsInput {
2
    meanRadius: Float!
3
    mass: MassInput!
4
    population: Float
5
}
6
 
          
7
input MassInput {
8
    number: Float!
9
    tenPower: Int!
10
}


Input type

As for a query, DataFetcher should be defined for a mutation:

Kotlin
xxxxxxxxxx
1
28
 
1
@Singleton
2
class CreatePlanetDataFetcher(
3
    private val objectMapper: ObjectMapper,
4
    private val planetService: PlanetService,
5
    private val planetConverter: PlanetConverter
6
) : DataFetcher<PlanetDto> {
7
8
    private val log = LoggerFactory.getLogger(this.javaClass)
9
10
    override fun get(env: DataFetchingEnvironment): PlanetDto {
11
        log.info("Trying to create planet")
12
13
        val name = env.getArgument<String>("name")
14
        val type = env.getArgument<Planet.Type>("type")
15
        val detailsInputDto = objectMapper.convertValue(env.getArgument("details"), DetailsInputDto::class.java)
16
17
        val newPlanet = planetService.create(
18
            name,
19
            type,
20
            detailsInputDto.meanRadius,
21
            detailsInputDto.mass.number,
22
            detailsInputDto.mass.tenPower,
23
            detailsInputDto.population
24
        )
25
26
        return planetConverter.toDto(newPlanet)
27
    }
28
}

DataFetcher for the mutation (source code)

Let’s suppose that someone wants to be notified of a planet adding event. For such a purpose subscription can be used:

Java
 




xxxxxxxxxx
1


 
1
type Subscription {
2
    latestPlanet: Planet!
3
}


Subscription (source code)

The subscription’s DataFetcher returns Publisher:

Kotlin
xxxxxxxxxx
1
 
1
@Singleton
2
class LatestPlanetDataFetcher(
3
    private val planetService: PlanetService,
4
    private val planetConverter: PlanetConverter
5
) : DataFetcher<Publisher<PlanetDto>> {
6
7
    override fun get(environment: DataFetchingEnvironment) = planetService.getLatestPlanet().map { planetConverter.toDto(it) }
8
}

DataFetcher for subscription (source code)

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):

Java
 




xxxxxxxxxx
1


 
1
subscription {
2
  latestPlanet {
3
    name
4
    type
5
  }
6
}


Request for subscription

In the second perform mutation like this:

Java
 




xxxxxxxxxx
1


 
1
mutation {
2
  createPlanet(
3
    name: "Pluto"
4
    type: DWARF_PLANET
5
    details: { meanRadius: 50.0, mass: { number: 0.0146, tenPower: 24 } }
6
  ) {
7
    id
8
  }
9
}


Request for mutation

The subscribed client will be notified of a planet creation:

mutation subscription

Subscriptions in Micronaut are enabled by using the following option:

YAML
xxxxxxxxxx
1
 
1
graphql:
2
  graphql-ws:
3
    enabled: true

Switching on GraphQL over WebSocket (source code)

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:

Kotlin
xxxxxxxxxx
1
35
 
1
@Test
2
fun testPlanets() {
3
    val query = """
4
        {
5
            planets {
6
                id
7
                name
8
                type
9
                details {
10
                    meanRadius
11
                    mass
12
                    ... on InhabitedPlanetDetails {
13
                        population
14
                    }
15
                }
16
            }
17
        }
18
    """.trimIndent()
19
20
    val response = graphQLClient.sendRequest(query, object : TypeReference<List<PlanetDto>>() {})
21
22
    assertThat(response, hasSize(8))
23
    assertThat(
24
        response, contains(
25
            hasProperty("name", `is`("Mercury")),
26
            hasProperty("name", `is`("Venus")),
27
            hasProperty("name", `is`("Earth")),
28
            hasProperty("name", `is`("Mars")),
29
            hasProperty("name", `is`("Jupiter")),
30
            hasProperty("name", `is`("Saturn")),
31
            hasProperty("name", `is`("Uranus")),
32
            hasProperty("name", `is`("Neptune"))
33
        )
34
    )
35
}

Query test (source code)

If a part of a query can be reused in another query, you can use fragments:

Kotlin
xxxxxxxxxx
1
32
 
1
private val planetFragment = """
2
    fragment planetFragment on Planet {
3
        id
4
        name
5
        type
6
        details {
7
            meanRadius
8
            mass
9
            ... on InhabitedPlanetDetails {
10
                population
11
            }
12
        }
13
    }
14
""".trimIndent()
15
16
@Test
17
fun testPlanetById() {
18
    val earthId = 3
19
    val query = """
20
        {
21
            planet(id: $earthId) {
22
                ... planetFragment
23
            }
24
        }
25
26
        $planetFragment
27
    """.trimIndent()
28
29
    val response = graphQLClient.sendRequest(query, object : TypeReference<PlanetDto>() {})
30
31
    // assertions
32
}

Query test using a fragment (source code)

To use variables, you can write tests in this way:

Kotlin
xxxxxxxxxx
1
17
 
1
@Test
2
fun testPlanetByName() {
3
    val variables = mapOf("name" to "Earth")
4
    val query = """
5
        query testPlanetByName(${'$'}name: String!){
6
            planetByName(name: ${'$'}name) {
7
                ... planetFragment
8
            }
9
        }
10
11
        $planetFragment
12
    """.trimIndent()
13
14
    val response = graphQLClient.sendRequest(query, variables, null, object : TypeReference<PlanetDto>() {})
15
16
    // assertions
17
}

Query test using a fragment and variables (source code)

This approach looks a little strange because in Kotlin raw strings, or string templates, you can’t escape a symbol, so to represent $ 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:

Java
 




xxxxxxxxxx
1
17


 
1
type Query {
2
    validateToken(token: String!): Boolean!
3
}
4
 
          
5
type Mutation {
6
    signIn(data: SignInData!): SignInResponse!
7
}
8
 
          
9
input SignInData {
10
    username: String!
11
    password: String!
12
}
13
 
          
14
type SignInResponse {
15
    username: String!
16
    token: String!
17
}


Schema of Auth service (source code)

To get a JWT you need to perform in a GraphQL IDE the following mutation (Auth service URL is http://localhost:8081/graphql):

Java
 




xxxxxxxxxx
1


 
1
mutation {
2
  signIn(data: {username: "john_doe", password: "password"}) {
3
    token
4
  }
5
}


Getting JWT

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:

Java
 




xxxxxxxxxx
1
25


 
1
type Query {
2
    satellites: [Satellite!]!
3
    satellite(id: ID!): Satellite
4
    satelliteByName(name: String!): Satellite
5
}
6
 
          
7
type Satellite {
8
    id: ID!
9
    name: String!
10
    lifeExists: LifeExists!
11
    firstSpacecraftLandingDate: Date
12
}
13
 
          
14
type Planet @key(fields: "id") @extends {
15
    id: ID! @external
16
    satellites: [Satellite!]!
17
}
18
 
          
19
enum LifeExists {
20
    YES,
21
    OPEN_QUESTION,
22
    NO_DATA
23
}
24
 
          
25
scalar Date


Schema of Satellite service (source code)

Say in the Satellite type field lifeExists 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):

YAML
xxxxxxxxxx
1
12
 
1
micronaut:
2
  security:
3
    enabled: true
4
    intercept-url-map:
5
      - pattern: /graphql
6
        httpMethod: POST
7
        access:
8
          - isAnonymous()
9
      - pattern: /graphiql
10
        httpMethod: GET
11
        access:
12
          - isAnonymous()

Security configuration (source code)

It is not recommended putting authorization logic into DataFetcher to not make an application’s logic brittle:

Kotlin
xxxxxxxxxx
1
 
1
@Singleton
2
class LifeExistsDataFetcher(
3
    private val satelliteService: SatelliteService
4
) : DataFetcher<Satellite.LifeExists> {
5
    override fun get(env: DataFetchingEnvironment): Satellite.LifeExists {
6
        val id = env.getSource<SatelliteDto>().id
7
        return satelliteService.getLifeExists(id)
8
    }
9
}

LifeExistsDataFetcher (source code)

Protection of a field can be done using a framework’s means and custom logic:

Kotlin
xxxxxxxxxx
1
19
 
1
@Singleton
2
class SatelliteService(
3
    private val repository: SatelliteRepository,
4
    private val securityService: SecurityService
5
) {
6
7
    // other stuff
8
9
    fun getLifeExists(id: Long): Satellite.LifeExists {
10
        val userIsAuthenticated = securityService.isAuthenticated
11
        if (userIsAuthenticated) {
12
            return repository.findById(id)
13
                .orElseThrow { RuntimeException("Can't find satellite by id=$id") }
14
                .lifeExists
15
        } else {
16
            throw RuntimeException("`lifeExists` property can only be accessed by authenticated users")
17
        }
18
    }
19
}

SatelliteService (source code)

The following request can only be successful if you will specify the Authorization header with received JWT (see the previous section):

Java
 




xxxxxxxxxx
1


 
1
{
2
  satellite(id: "1") {
3
    name
4
    lifeExists
5
  }
6
}


Request for protected field

The service validates token automatically using the framework. The secret is stored in the configuration file (in the Base64 form):

YAML
xxxxxxxxxx
1
13
 
1
micronaut:
2
  security:
3
    token:
4
      jwt:
5
        enabled: true
6
        signatures:
7
          secret:
8
            validation:
9
              base64: true
10
              # In real life, the secret should NOT be under source control (instead of it, for example, in environment variable).
11
              # It is here just for simplicity.
12
              secret: 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdA=='
13
              jws-algorithm: HS256

JWT configuration (source code)

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:

Kotlin
xxxxxxxxxx
1
 
1
private fun createRuntimeWiring(): RuntimeWiring = RuntimeWiring.newRuntimeWiring()
2
    // other stuff
3
    .scalar(ExtendedScalars.Date)
4
    .build()

Registration of an additional scalar type (source code)

Then they can be used as others:

Java
 




xxxxxxxxxx
1


 
1
{
2
  satelliteByName(name: "Moon") {
3
    firstSpacecraftLandingDate
4
  }
5
}


Request

JSON
xxxxxxxxxx
1
 
1
{
2
  "data": {
3
    "satelliteByName": {
4
      "firstSpacecraftLandingDate": "1959-09-13"
5
    }
6
  }
7
}

Response

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:

Java
 




xxxxxxxxxx
1
15


 
1
{
2
  planet(id: "1") {
3
    star {
4
      planets {
5
        star {
6
          planets {
7
            star {
8
              ... # more deep nesting!
9
            }
10
          }
11
        }
12
      }
13
    }
14
  }
15
}


Example of expensive query

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):

Kotlin
xxxxxxxxxx
1
14
 
1
return GraphQL.newGraphQL(transformedGraphQLSchema)
2
    // other stuff
3
    .instrumentation(
4
        ChainedInstrumentation(
5
            listOf(
6
                FederatedTracingInstrumentation(),
7
                MaxQueryComplexityInstrumentation(50, FieldComplexityCalculator { env, child ->
8
                    1 + child
9
                }),
10
                MaxQueryDepthInstrumentation(5)
11
            )
12
        )
13
    )
14
    .build()

Setup an instrumentation (source code)

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 size 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:

Kotlin
xxxxxxxxxx
1
15
 
1
@Singleton
2
// mark it as primary to override the default one
3
@Primary
4
class HeaderValueProviderGraphQLExecutionInputCustomizer : DefaultGraphQLExecutionInputCustomizer() {
5
6
    override fun customize(executionInput: ExecutionInput, httpRequest: HttpRequest<*>): Publisher<ExecutionInput> {
7
        val context = HTTPRequestHeaders { headerName ->
8
            httpRequest.headers[headerName]
9
        }
10
11
        return Publishers.just(executionInput.transform {
12
            it.context(context)
13
        })
14
    }
15
}

Example of GraphQLExecutionInputCustomizer (source code)

This customizer provides an ability to FederatedTracingInstrumentation to check whether a request has come from Apollo Server and 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:

Kotlin
xxxxxxxxxx
1
20
 
1
@Singleton
2
class CustomDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() {
3
4
    private val log = LoggerFactory.getLogger(this.javaClass)
5
6
    override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult {
7
        val exception = handlerParameters.exception
8
        log.error("Exception while GraphQL data fetching", exception)
9
10
        val error = object : GraphQLError {
11
            override fun getMessage(): String = "There was an error: ${exception.message}"
12
13
            override fun getErrorType(): ErrorType? = null
14
15
            override fun getLocations(): MutableList<SourceLocation>? = null
16
        }
17
18
        return DataFetcherExceptionHandlerResult.newResult().error(error).build()
19
    }
20
}

Custom exception handler (source code)

The main purpose of the service is to demonstrate how the distributed GraphQL entity (Planet) can be resolved in two services and then accessed through Apollo Server. Planet type was earlier defined in the Planet service in this way:

Java
 




xxxxxxxxxx
1


 
1
type Planet @key(fields: "id") {
2
    id: ID!
3
    name: String!
4
    # from an astronomical point of view
5
    type: Type!
6
    isRotatingAroundSun: Boolean! @deprecated(reason: "Now it is not in doubt. Do not use this field")
7
    details: Details!
8
}


Definition of Planet type in Planet service (source code)

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:

Java
 




xxxxxxxxxx
1
11


 
1
type Satellite {
2
    id: ID!
3
    name: String!
4
    lifeExists: LifeExists!
5
    firstSpacecraftLandingDate: Date
6
}
7
 
          
8
type Planet @key(fields: "id") @extends {
9
    id: ID! @external
10
    satellites: [Satellite!]!
11
}


Extension of Planet type in Satellite service (source code)

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:

  • GraphQL Kotlin

    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.

  • Apollo Federation on the JVM

    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:

Kotlin
xxxxxxxxxx
1
20
 
1
@Bean
2
@Singleton
3
fun graphQL(resourceResolver: ResourceResolver): GraphQL {
4
5
    // other stuff
6
7
    val planetEntityResolver = object : FederatedEntityResolver<Long, PlanetDto>("Planet", { id ->
8
        log.info("`Planet` entity with id=$id was requested")
9
        val satellites = satelliteService.getByPlanetId(id)
10
        PlanetDto(id = id, satellites = satellites.map { satelliteConverter.toDto(it) })
11
    }) {}
12
13
    val transformedGraphQLSchema = FederatedSchemaBuilder()
14
        .schemaInputStream(schemaInputStream)
15
        .runtimeWiring(createRuntimeWiring())
16
        .federatedEntitiesResolvers(listOf(planetEntityResolver))
17
        .build()
18
19
    // other stuff
20
}

Definition of GraphQL bean in Satellite service (source code)

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:

JSON
xxxxxxxxxx
1
16
 
1
{
2
  "name": "api-gateway",
3
  "main": "gateway.js",
4
  "scripts": {
5
    "start-gateway": "nodemon gateway.js"
6
  },
7
  "devDependencies": {
8
    "concurrently": "5.1.0",
9
    "nodemon": "2.0.2"
10
  },
11
  "dependencies": {
12
    "@apollo/gateway": "0.12.0",
13
    "apollo-server": "2.10.0",
14
    "graphql": "14.6.0"
15
  }
16
}

Meta information, dependencies, and other (source code)

JavaScript
xxxxxxxxxx
1
29
 
1
const {ApolloServer} = require("apollo-server");
2
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");
3
4
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
5
    willSendRequest({request, context}) {
6
        request.http.headers.set('Authorization', context.authHeaderValue);
7
    }
8
}
9
10
const gateway = new ApolloGateway({
11
    serviceList: [
12
        {name: "auth-service", url: "http://localhost:8081/graphql"},
13
        {name: "planet-service", url: "http://localhost:8082/graphql"},
14
        {name: "satellite-service", url: "http://localhost:8083/graphql"}
15
    ],
16
    buildService({name, url}) {
17
        return new AuthenticatedDataSource({url});
18
    },
19
});
20
21
const server = new ApolloServer({
22
    gateway, subscriptions: false, context: ({req}) => ({
23
        authHeaderValue: req.headers.authorization
24
    })
25
});
26
27
server.listen().then(({url}) => {
28
    console.log(`�� Server ready at ${url}`);
29
});

Apollo Server definition (source code)

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:

Shell
xxxxxxxxxx
1
 
1
npm install
2
npm run start-gateway


A successful launch should look like this:

Plain Text
xxxxxxxxxx
1
 
1
[nodemon] 2.0.2
2
[nodemon] to restart at any time, enter `rs`
3
[nodemon] watching dir(s): *.*
4
[nodemon] watching extensions: js,mjs,json
5
[nodemon] starting `node gateway.js`
6
� Server ready at http://localhost:4000/
7
[INFO] Sat Feb 15 2020 13:22:37 GMT+0300 (Moscow Standard Time) apollo-gateway: Gateway successfully loaded schema.
8
        * Mode: unmanaged

Apollo Server startup log

Then you can use a unified interface to perform GraphQL requests to all of your services:

altair apollo server

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!

Further Reading

Smart Pipes and Smart Endpoints With Service Mesh

Microservices With GraphQL

GraphQL Kotlin (programming language) Database microservice Web Service code style Java (programming language) Schema Requests Library

Published at DZone with permission of Roman Kudryashov. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How To Build Web Service Using Spring Boot 2.x
  • The First Annual Recap From JPA Buddy
  • Why and When to Use GraphQL
  • How to Create Microservices Using http4k

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!