Micronaut GraphQL Integration

Extensions to integrate Micronaut and GraphQL

Version:

1 Introduction

Micronaut supports GraphQL via the micronaut-graphql module.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Breaking Changes

Version 4.0.0

The Apollo Websocket protocol (subscriptions-transport-ws) classes and configuration have been refactored and deprecated to pave the way for a newer websocket protocol (graphql-ws). The subscriptions-transport-ws protocol will be removed in a future version and client code should be migrated to use the new protocol. To continue using the subscriptions-transport-ws protocol, the following must be considered when upgrading:

  • The configuration prefix for subscriptions-transport-ws is changed from graphql-ws to graphql-apollo-ws

  • The implementation classes for subscriptions-transport-ws have moved from the io.micronaut.configuration.graphql.ws package to io.micronaut.configuration.graphql.ws.apollo. The implementation for the newer protocol has taken their place in io.micronaut.configuration.graphql.ws

  • The implementation classes for subscriptions-transport-ws have been renamed from GraphQLWs* to GraphQLApolloWs*. For example, GraphQLWsConfiguration is now GraphQLApolloWsConfiguration.

4 Quick Start

Create your application via the Command Line tool:

mn create-app helloworld --features=graphql

If you already have an application, add the micronaut graphql dependency:

implementation("io.micronaut.graphql:micronaut-graphql")
<dependency>
    <groupId>io.micronaut.graphql</groupId>
    <artifactId>micronaut-graphql</artifactId>
</dependency>

Configure the /graphql endpoint by adding to application.yml:

graphql:
  enabled: true
  graphiql: # enables the /graphiql endpoint to test calls against your graph.
    enabled: true

And then in the resources folder, create a file named schema.graphqls. This file will contain the definition of your GraphQL schema. In our case, it will contain the following:

type Query {
    hello(name: String): String!
}

Create a DataFetcher for the hello query:

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import jakarta.inject.Singleton;

@Singleton
public class HelloDataFetcher implements DataFetcher<String> {

    @Override
    public String get(DataFetchingEnvironment env) {
        String name = env.getArgument("name");
        if (name == null || name.trim().length() == 0) {
            name = "World";
        }
        return String.format("Hello %s!", name);
    }
}
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import groovy.transform.CompileStatic
import jakarta.inject.Singleton

@Singleton
@CompileStatic
class HelloDataFetcher implements DataFetcher<String> {

    @Override
    String get(DataFetchingEnvironment env) {
        String name = env.getArgument("name")
        name = name?.trim() ?: "World"
        return "Hello ${name}!"
    }
}
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import jakarta.inject.Singleton

@Singleton
class HelloDataFetcher : DataFetcher<String> {

    override fun get(env: DataFetchingEnvironment): String {
        var name = env.getArgument<String>("name")
        if (name == null || name.trim().isEmpty()) {
            name = "World"
        }
        return "Hello $name!"
    }
}

And then create the GraphQL bean:

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.io.ResourceResolver;
import jakarta.inject.Singleton;

import java.io.BufferedReader;
import java.io.InputStreamReader;

@Factory // (1)
public class GraphQLFactory {

    @Bean
    @Singleton
    public GraphQL graphQL(ResourceResolver resourceResolver, HelloDataFetcher helloDataFetcher) { // (2)

        SchemaParser schemaParser = new SchemaParser();
        SchemaGenerator schemaGenerator = new SchemaGenerator();

        // Parse the schema.
        TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
        typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))));

        // Create the runtime wiring.
        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher))
                .build();

        // Create the executable schema.
        GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build();
    }
}
import graphql.GraphQL
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.core.io.ResourceResolver
import jakarta.inject.Singleton

@Factory // (1)
@CompileStatic
class GraphQLFactory {

    @Bean
    @Singleton
    GraphQL graphQL(ResourceResolver resourceResolver, HelloDataFetcher helloDataFetcher) { // (2)

        def schemaParser = new SchemaParser()
        def schemaGenerator = new SchemaGenerator()

        // Parse the schema.
        def typeRegistry = new TypeDefinitionRegistry()
        typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))))

        // Create the runtime wiring.
        def runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", { typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher) })
                .build()

        // Create the executable schema.
        def graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring)

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build()
    }
}
import graphql.GraphQL
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.core.io.ResourceResolver
import jakarta.inject.Singleton
import java.io.BufferedReader
import java.io.InputStreamReader

@Factory // (1)
class GraphQLFactory {

    @Bean
    @Singleton
    fun graphQL(resourceResolver: ResourceResolver, helloDataFetcher: HelloDataFetcher): GraphQL { // (2)

        val schemaParser = SchemaParser()
        val schemaGenerator = SchemaGenerator()

        // Parse the schema.
        val typeRegistry = TypeDefinitionRegistry();
        typeRegistry.merge(schemaParser.parse(BufferedReader(InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))))

        // Create the runtime wiring.
        val runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query") { typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher) }
                .build()

        // Create the executable schema.
        val graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring)

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build()
    }
}

You should be all set. Start your application by running ./gradlew run, open your browser to your local graphiql, and you should be able to run the following queries:

Query without params:

query {
    hello
}

Returns:

{
  "data": {
    "hello": "Hello World!"
  }
}

Query with params:

query {
    hello(name: "Micronaut")
}

Returns:

{
  "data": {
    "hello": "Hello Micronaut!"
  }
}

5 Configuration

Micronaut 1.0.3 or above is required and you must have the micronaut-graphql dependency on your classpath:

implementation("io.micronaut.graphql:micronaut-graphql")
<dependency>
    <groupId>io.micronaut.graphql</groupId>
    <artifactId>micronaut-graphql</artifactId>
</dependency>

The micronaut-graphql module transitively includes the com.graphql-java:graphql-java dependency and provides a Micronaut GraphQLController which enables query execution via HTTP.

As outlined in https://graphql.org/learn/serving-over-http the following HTTP requests are supported:

  • GET request with query, operationName and variables query parameters. The variables query parameter must be json encoded.

  • POST request with application/json body and keys query (String), operationName (String) and variables (Map).

Both produce a application/json response.

By default the GraphQL endpoint is exposed on /graphql but this can be changed via the graphql.path application property.

src/main/resources/application.yml
graphql:
  enabled: true (1)
  path: /graphql (2)
1 Enables/disables the GraphQL integration. Default true.
2 Configures the GraphQL endpoint path. Default /graphql.

You only must configure a bean of type graphql.GraphQL containing the GraphQL schema and runtime wiring.

5.1 Configuring the GraphQL Bean

The graphql.GraphQL bean can be defined by solely using the GraphQL Java implementation, or in combination with other integration libraries like GraphQL Java Tools or GraphQL SPQR. As mentioned before the first one is added as transitive dependency, other integration libraries must be added to the classpath manually.

Below is a typical example of a Micronaut Factory class configuring a graphql.GraphQL Bean using the GraphQL Java library.

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.io.ResourceResolver;
import jakarta.inject.Singleton;

import java.io.BufferedReader;
import java.io.InputStreamReader;

@Factory // (1)
public class GraphQLFactory {

    @Bean
    @Singleton
    public GraphQL graphQL(ResourceResolver resourceResolver, HelloDataFetcher helloDataFetcher) { // (2)

        SchemaParser schemaParser = new SchemaParser();
        SchemaGenerator schemaGenerator = new SchemaGenerator();

        // Parse the schema.
        TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
        typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))));

        // Create the runtime wiring.
        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher))
                .build();

        // Create the executable schema.
        GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build();
    }
}
import graphql.GraphQL
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.core.io.ResourceResolver
import jakarta.inject.Singleton

@Factory // (1)
@CompileStatic
class GraphQLFactory {

    @Bean
    @Singleton
    GraphQL graphQL(ResourceResolver resourceResolver, HelloDataFetcher helloDataFetcher) { // (2)

        def schemaParser = new SchemaParser()
        def schemaGenerator = new SchemaGenerator()

        // Parse the schema.
        def typeRegistry = new TypeDefinitionRegistry()
        typeRegistry.merge(schemaParser.parse(new BufferedReader(new InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))))

        // Create the runtime wiring.
        def runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", { typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher) })
                .build()

        // Create the executable schema.
        def graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring)

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build()
    }
}
import graphql.GraphQL
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.SchemaGenerator
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.core.io.ResourceResolver
import jakarta.inject.Singleton
import java.io.BufferedReader
import java.io.InputStreamReader

@Factory // (1)
class GraphQLFactory {

    @Bean
    @Singleton
    fun graphQL(resourceResolver: ResourceResolver, helloDataFetcher: HelloDataFetcher): GraphQL { // (2)

        val schemaParser = SchemaParser()
        val schemaGenerator = SchemaGenerator()

        // Parse the schema.
        val typeRegistry = TypeDefinitionRegistry();
        typeRegistry.merge(schemaParser.parse(BufferedReader(InputStreamReader(
                resourceResolver.getResourceAsStream("classpath:schema.graphqls").get()))))

        // Create the runtime wiring.
        val runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query") { typeWiring -> typeWiring
                        .dataFetcher("hello", helloDataFetcher) }
                .build()

        // Create the executable schema.
        val graphQLSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring)

        // Return the GraphQL bean.
        return GraphQL.newGraphQL(graphQLSchema).build()
    }
}
1 Define the Factory annotation to create the bean.
2 Define the GraphQL bean which contains the runtime wiring and the executable schema.

There are various examples using different technologies provided in the repository.

5.2 Configuring GraphQL over websockets

The micronaut-graphql module comes bundled with support for GraphQL over web sockets.

Support is provided both for the both current graphql-ws protocol (https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) and the now deprecated subscriptions-transport-ws protocol from Apollo (https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md).

GraphQL over web sockets via the current graphql-ws protocol must be explicitly enabled via the graphql.graphql-ws.enabled application property.

The following configuration properties can be set for the graphql-ws support:

src/main/resources/application.yml
graphql:
  graphql-ws:
    enabled: false (1)
    path: /graphql-ws (2)
    connection-init-wait-timeout: 5s (3)
1 Enables/disables the graphql-ws implementation of GraphQL over web sockets. Default false.
2 Configures the graphql-ws endpoint path. Default /graphql-ws.
3 Configures the maximum time allowed for a client to initiate a graphql-ws connection after the WebSocket is initially opened. Default 15s.

The deprecated subscriptions-transport-ws support must be explicitly enabled via the graphql.graphql-apollo-ws.enabled application property.

Prior to version 4.0 of this module, the subscriptions-transport-ws protocol was the only supported implementation, and its configuration was set via the graphql.graphql-ws property path. This configuration must be migrated to the graphql.graphql-apollo-ws if you have existing client code that depends on this protocol.

While the subscriptions-transport-ws implementation is capable of also handling queries and mutations over WebSocket it might not be supported in all clients. Some clients have a way of configuring a different endpoint for subscriptions and/or some filter to only use the websocket for subscriptions.

The following configuration properties can be set for the subscriptions-transport-ws support:

src/main/resources/application.yml
graphql:
  graphql-apollo-ws:
    enabled: false (1)
    path: /graphql-ws (2)
    keep-alive-enabled: true (3)
    keep-alive-interval: 15s (4)
1 Enables/disables GraphQL over web sockets. Default false.
2 Configures the GraphQLApolloWs endpoint path. Default /graphql-ws.
3 Enables/disables keep alive, this might be needed to prevent clients reconnecting. Default true.
4 Configures the keep alive interval, specific clients might need different values, or it could be set higher to reduce some load 15s.

There is an example present chat, that features a very basic chat application. For real applications the subscriptions are usually based on some pub/sub solution. An example using subscriptions with kafka can be found here, graphql-endpoint using micronaut.

5.3 Configuring GraphiQL

The micronaut-graphql module comes bundled with GraphiQL, an in-browser IDE for exploring GraphQL.

GraphiQL

GraphiQL must be explicitly enabled via the graphql.graphiql.enabled application property.

The following configuration properties can be set:

🔗
Table 1. Configuration Properties for GraphQLConfiguration$GraphiQLConfiguration
Property Type Description

graphql.graphiql.enabled

boolean

Returns whether GraphiQL is enabled. Default value (false).

graphql.graphiql.version

java.lang.String

Returns the GraphiQL version. Default value ("3.0.6").

graphql.graphiql.explorer-plugin-version

java.lang.String

Returns the GraphIQL Explorer plugin version. Default value ("0.3.5").

graphql.graphiql.path

java.lang.String

Returns the GraphiQL path. Default value ("/graphiql").

graphql.graphiql.template-path

java.lang.String

Returns the GraphiQL template path. Default value ("classpath:graphiql/index.html").

graphql.graphiql.template-parameters

java.util.Map

Returns the GraphiQL template parameters to be substituted in the template.

graphql.graphiql.page-title

java.lang.String

Returns the GraphiQL page title. Default value ("GraphiQL").

The out of the box rendered GraphiQL page does not provide many customisations except the GraphiQL version, path and page title. It also takes into account the graphql.path application property, to provide a seamless integration with the configured GraphQL endpoint path.

If further customisations are required, a custom GraphiQL template can be provided. Either by providing the custom template at src/main/resources/graphiq/index.html or via the graphiql.template-path application property pointing to a different template location. In that case it could also be useful to dynamically replace additional parameters in the template via the graphql.graphiql.template-parameters application property.

5.4 Notes on using Jackson serialization

If you are using Jackson serialization instead of Micronaut Serialization, you need to configure your application to keep empty and null values in the serialized JSON.

This is done via:

jackson.serialization-inclusion=ALWAYS
jackson:
    serialization-inclusion: ALWAYS
[jackson]
  serialization-inclusion="ALWAYS"
jackson {
  serializationInclusion = "ALWAYS"
}
{
  jackson {
    serialization-inclusion = "ALWAYS"
  }
}
{
  "jackson": {
    "serialization-inclusion": "ALWAYS"
  }
}

6 Guides

See the following list of guides to learn more about working with GraphQL in the Micronaut Framework:

7 Repository

You can find the source code of this project in this repository: