Skip to content

Repository files navigation

Chat at https://discord.gg/sgak4e5NKv Build License

About Javalin

Javalin GraphQL

Serve a GraphQL schema, GraphiQL and subscriptions from a Javalin application. Schemas are generated from Kotlin (or Java) classes by graphql-kotlin.

Javalin 7.x
graphql-kotlin 10.x
JVM 17+

Upgrading from 5.x? The public API changed. See Migrating from 5.x.

Getting Started

7.0.0 is published to Javalin's Reposilite, not to Maven Central. See #7 for the Maven Central discussion, and the changelog for what is in this release.

Add the dependency:

Gradle
repositories {
    maven("https://maven.reposilite.com/releases")
}

dependencies {
    implementation("io.javalin.community.graphql:javalin-graphql:7.0.0")
}
Maven
<repositories>
    <repository>
        <id>reposilite-releases</id>
        <url>https://maven.reposilite.com/releases</url>
    </repository>
</repositories>

<dependency>
    <groupId>io.javalin.community.graphql</groupId>
    <artifactId>javalin-graphql</artifactId>
    <version>7.0.0</version>
</dependency>

Javalin 7 does not bundle an object mapper, and this plugin uses the one your application configures. If you have not set one up, add jackson-databind (plus jackson-module-kotlin if your schema classes are written in Kotlin).

Register the plugin:

val app = Javalin.create { config ->
    val plugin = GraphQLPluginBuilder("/graphql")
        .addPackage("com.example.schema")
        .register(QueryExample())
        .register(MutationExample())
        .register(SubscriptionExample())
        .build()

    config.registerPlugin(plugin)
}

app.start(7070)

The same builder works from Java, where Kotlin's default arguments do not exist, because its constructor is annotated with @JvmOverloads:

GraphQLPlugin plugin = new GraphQLPluginBuilder("/graphql")
    .addPackage("com.example.schema")
    .register(new QueryExample())
    .build();

Javalin.create(config -> config.registerPlugin(plugin)).start(7070);

GraphQL is now served under /graphql: GET returns GraphiQL, POST executes queries and mutations, and the WebSocket on the same path serves subscriptions. Call disableGraphiQL() on the builder to leave out the GET route and serve only the endpoint.

Queries

@GraphQLDescription("Query Example")
class QueryExample : QueryGraphql {
    fun hello(): String = "Hello world"

    fun demoData(@GraphQLDescription("awesome input") data: DemoData): DemoData = data
}

Mutations

@GraphQLDescription("Mutation Example")
class MutationExample(private var message: String) : MutationGraphql {
    fun changeMessage(newMessage: String): String {
        message = newMessage
        return message
    }
}

Subscriptions

A subscription resolver returns a kotlinx Flow:

@GraphQLDescription("Subscription Example")
class SubscriptionExample : SubscriptionGraphql {
    fun counter(): Flow<Int> = flow {
        while (true) {
            delay(100)
            emit(1)
        }
    }
}

Every class has to be registered when the plugin is built.

Subscriptions are served over the graphql-transport-ws protocol, so a standard client such as graphql-ws can talk to the endpoint directly. The connection is acknowledged with connection_init / connection_ack, each operation carries an id, and a subscription is cancelled by the client's complete message or when the socket closes.

Javalin exposes no API for WebSocket subprotocol negotiation, and the Jetty handshake underneath echoes back whichever subprotocol the client requested first, without checking it. A graphql-transport-ws client connects correctly; a client asking for a protocol this plugin does not speak — the legacy graphql-ws subprotocol, say — is told yes and then receives messages it cannot understand.

Context

Build a context by putting values into graphql-java's GraphQLContext, keyed by class:

data class MyContext(val authorization: String?) {
    val isValid = authorization != null
}

class MyContextFactory : GraphQLContextFactory<Context> {
    override suspend fun generateContext(request: Context): GraphQLContext =
        mapOf(MyContext::class to MyContext(request.header("Authorization")))
            .toGraphQLContext()
}

Read it in a resolver through the DataFetchingEnvironment:

class QueryExample : QueryGraphql {
    fun isAuthorized(environment: DataFetchingEnvironment): Boolean =
        environment.graphQlContext.get<MyContext>(MyContext::class)?.isValid == true
}

Context factories are passed to the builder, one for HTTP and one for subscriptions. Both are optional and default to an empty context:

val plugin = GraphQLPluginBuilder("/graphql", MyContextFactory(), MyWsContextFactory())
    .addPackage("com.example.schema")
    .register(QueryExample())
    .build()

config.registerPlugin(plugin)

Writing a context factory from Java is not practical: generateContext is a suspend function. Java applications use the defaults.

JPMS

The artifact declares Automatic-Module-Name: io.javalin.community.graphql, so a modular application can depend on it:

requires io.javalin.community.graphql;

It does not ship a module-info.java yet. The graphql-kotlin artifacts declare neither a module descriptor nor an Automatic-Module-Name, so they resolve as automatic modules named after their file names; requiring those would freeze unstable names into the descriptor. See #5.

Design decisions

The reasoning behind the 7.0 design is recorded as ADRs in docs/adr:

ADR-001 Context model follows graphql-java instead of a plugin type
ADR-002 Serve subscriptions over graphql-transport-ws
ADR-003 Parse GraphQL payloads without graphql-kotlin's sealed types
ADR-004 Declare an automatic module name instead of shipping module-info
ADR-005 Keep the QueryGraphql / MutationGraphql / SubscriptionGraphql markers

Migrating from 5.x

Registering the plugin. config.plugins.register(...) became config.registerPlugin(...).

GraphQLOptions is gone. GraphQLPluginBuilder is the only entry point, and its context factories are optional:

- val options = GraphQLOptions("/graphql")
-     .addPackage("com.example.schema")
-     .register(QueryExample())
- config.registerPlugin(GraphQLPlugin(options))
+ config.registerPlugin(
+     GraphQLPluginBuilder("/graphql")
+         .addPackage("com.example.schema")
+         .register(QueryExample())
+         .build()
+ )

GraphQLPluginBuilder.create(options) and the GraphQLPlugin(options) constructor went with it, as did middleHandler, wsMiddleHandler, setMiddleHandler, setWSMiddleHandler and the context constructor argument. Nothing ever read any of those four: a middleHandler set on the options was silently dropped when the builder was created from them. Javalin's own before and beforeWs do the job they promised.

GraphQLPluginBuilder.add(...) is now addPackage(...), the name GraphQLOptions used.

At least one package is now required. The default used to be kotlin.Unit, which is a class rather than a package and matched nothing; a builder with no addPackage call now fails at start-up saying so.

Context is no longer a type of yours. graphql-kotlin removed its GraphQLContext marker interface in favour of graphql-java's map-like GraphQLContext.

- data class MyContext(val authorization: String?) : GraphQLContext
+ data class MyContext(val authorization: String?)

- class MyContextFactory : GraphQLContextFactory<MyContext, Context> {
-     override suspend fun generateContext(request: Context): MyContext =
-         MyContext(request.header("Authorization"))
+ class MyContextFactory : GraphQLContextFactory<Context> {
+     override suspend fun generateContext(request: Context): GraphQLContext =
+         mapOf(MyContext::class to MyContext(request.header("Authorization")))
+             .toGraphQLContext()
  }

GraphQLPluginBuilder lost its context type parameter as a result.

Context is no longer injected into resolvers. A resolver parameter typed as your context class is now treated as a GraphQL argument. Take a DataFetchingEnvironment instead:

- fun isAuthorized(context: MyContext?): Boolean = context?.isValid == true
+ fun isAuthorized(environment: DataFetchingEnvironment): Boolean =
+     environment.graphQlContext.get<MyContext>(MyContext::class)?.isValid == true

Subscriptions return Flow, not Publisher. Reactor is no longer a dependency.

- fun counter(): Flux<Int> = Flux.interval(Duration.ofMillis(100)).map { 1 }
+ fun counter(): Flow<Int> = flow { while (true) { delay(100); emit(1) } }

Subscriptions speak graphql-transport-ws. 5.x used an ad-hoc exchange — send a query frame, receive bare result data — which matched no standard. A client now has to send connection_init and subscribe messages. Off-the-shelf GraphQL clients do this for you.

Removed: GraphQLOptions, GraphQLRun (use GraphQLRequestHandler.executeSubscription) and JavalinDataLoaderRegistryFactory (use graphql-kotlin's KotlinDataLoaderRegistryFactory).

About

Artifact/module for enabling graphql support in Javalin

Resources

Contributing

Stars

7 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages