- ❤️ Sponsor Javalin
- The main project webpage is javalin.io
- Chat on Discord: https://discord.gg/sgak4e5NKv
- License summary: https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
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.
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.
@GraphQLDescription("Query Example")
class QueryExample : QueryGraphql {
fun hello(): String = "Hello world"
fun demoData(@GraphQLDescription("awesome input") data: DemoData): DemoData = data
}@GraphQLDescription("Mutation Example")
class MutationExample(private var message: String) : MutationGraphql {
fun changeMessage(newMessage: String): String {
message = newMessage
return message
}
}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-wsclient connects correctly; a client asking for a protocol this plugin does not speak — the legacygraphql-wssubprotocol, say — is told yes and then receives messages it cannot understand.
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:
generateContextis asuspendfunction. Java applications use the defaults.
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.
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 |
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 == trueSubscriptions 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).