I started looking into GraphQL not that long ago and a project came my way that needed a server as well as modern web client. GraphQL made perfect sense to me at this point and since I already knew Kotlin from my Android work, I wanted to use it to build a GraphQL server.
The requirements for this server was simple:
- It needed to have CRUD operations on database resources.
- It needed to have a permissions system to limit access/changes to database resources.
- It needed to be simple.
Defining Our Routes
The simplest way to build a Kotlin server is using Ktor. It has a simple DSL to define routes and it also simplifies tasks like authenticating requests.
Here’s one way to define routes for this application
package me.amryousef
// imports removed for brevity
private val gson = Gson()
fun main(args: Array<String>): Unit {
io.ktor.server.netty.EngineMain.main(args)
}
@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
install(CallLogging) {
// removed for brevity
}
install(CORS) {
// removed for brevity
}
install(DefaultHeaders) {
// removed for brevity
}
install(ContentNegotiation) {
val converter = GsonConverter(gson)
register(ContentType.Application.Json, converter)
}
routing {
static("/") {
default("index.html")
}
authenticate(optional = true) {
route("/graphql") {
post {
// GraphQL route to handle queries and mutations
}
}
}
}
}
Setting Up Authentication Feature
The most important thing to note here is that we defined two routes. The root route will server a static html file which has GraphiQL ui. The second route is the one that will handle GraphQL mutations and queries. You will notice that it is wrapped with optional authenticate
which means that requests made to this route will need to pass by an authentication validation logic before being processed.
One of the smartest things about Ktor is the concept of features. It gives you the option to start with a very simple server while having the option to add features in to make more powerful. For example, the code above defines a ContentNegotiation
which is used by Ktor to format responses.
install(ContentNegotiation) {
val converter = GsonConverter(gson)
register(ContentType.Application.Json, converter)
}
If you look at this piece of code you will see that it install the feature and registers a converter for JSON responses. This converter uses an instant of Gson to convert POJOs to JSON.
Similarly, we can define an Authentication feature. For example, in my project, I installed a JWT authentication feature like this:
install(Authentication) {
jwt {
realm = "localhost"
// TokenManager is my own token handling class
// It handles creating and verifying tokens
verifier(tokenManager.verifier)
validate { credentials ->
// Add you validation code for the Jwt token
// You can access the token from 'credentials.payload'
// Remember that you need to return a 'Principal' here
}
}
}
You can see from this code that allows you to pass a verifier to handle the verifying the authenticity of the token. It also allows you to add your own validation token and return whatever value the represents the authentication state. Here’s mine as an example.
sealed class TokenValidity {
data class Valid(
val user: User,
val tokenType: TokenType
) : TokenValidity(), Principal
object NotValid : TokenValidity()
}
In the validate
block I either return a TokenValidity.Valid
as my principal or null. Which means that in the /graphql
route, I am able to check if a principal exists before continuing.
...
authenticate(optional = true) {
route("/graphql") {
post {
val user = call.authentication.principal<JwtTokenManager.TokenValidity.Valid>()
?: JwtTokenManager.TokenValidity.NotValid
}
}
}
....
One we add GraphQL support, we can build login and token refresh functionality with ease.
Finally, here’s an example of the TokenManager
I am using
Adding GraphQL Support
So far we have defined the routes we need and added JWT-based authentication. We now need to add GraphQL support. I’ve used KGraphQL which is an actively maintained Kotlin implementation. The thing that encouraged me to use this implementation is that its DSL fits really well with Ktor.
The starting point point for GraphQL support is to define the schema. KGraphQL has a simple DSL for that .
data class User(val id: Int, val name: String)
val schema = KGraphQL.schema {
// Define your types here
type<User>()
mutation("login") {
resolver { username: String, password: String ->
// Do your login logic
// returns User
}
}
query("getUsers") { isActive: Boolean ->
// Do your get users logic
// returns List<User>
}
}
In this schema we have a defined a type called User
which has and id
and a name
. This means that when the query or the mutation are executed, we will automatically get this json.
{
"id": 1,
"name": "Amr Yousef"
}
And to hook it all up with Ktor, we can just use the schema whenever the route is hit.
data class GraphQLRequest(val query: String = "", val variables: JsonObject = JsonObject())
...
authenticate(optional = true) {
route("/graphql") {
post {
val user = call.authentication.principal<JwtTokenManager.TokenValidity.Valid>()
?: JwtTokenManager.TokenValidity.NotValid
val schema = ....
val request = call.receive<GraphQLRequest>()
schema.execute(
query,
request.variables.toString(),
context{
+user
}
)
}
}
}
....
When a GraphQL sends a request for a query or a mutation, it sends a JSON like this:
{
"operationName": "getUsers",
"variables": {
"isActive": true
},
"query": ".... (removed for brevity)"
}
That’s why we have this GraphQLRequest
data class to map requests and make them suitable for processing via KGraphQL.
One thing we also include in the KGraphQL
context is the logged in user. This will allow us to add custom logic on who can execute the query/mutation.
Here’s a great article to expand more on GraphQL by Annyce Davis. I used it for reference when building this project.
Adding a Permissions System
So far we have configured our Ktor routes, added authentication and defined our GraphQL schema. The final step is to define a simple permissions system to define access rights for queries and mutations.
To do this, I opted into defining a clear set of roles that are tied to a user, for example:
enum class UserType {
ADMIN, NON_ADMIN
}
data class User(val id: Int, val name: String, val type; UserType)
Then added a two extension functions on SchemaBuilder
, which is the DSL builder used to build the schema, to define an authentication query and an authenticated mutation. I ended up with something like this:
fun SchemaBuilder.authenticatedQuery(
name: String,
forRoles: List<UserRole>,
init: QueryDSL.() -> Unit
) = query(name) {
authenticate(forRoles)
init()
}
fun SchemaBuilder.authenticatedMutation(
name: String,
forRoles: List<UserRole>,
init: MutationDSL.() -> Unit
) = mutation(name) {
authenticate(forRoles)
init()
}
@Throws(InvalidActionForRoleException::class, UnAuthorizedUserException::class)
private fun AbstractOperationDSL.authenticate(forRoles: List<UserRole>) = accessRule {
val validToken = it.get<JwtTokenManager.TokenValidity>() as? JwtTokenManager.TokenValidity.Valid
?: return@accessRule UnAuthorizedUserException()
if (validToken.tokenType != TokenType.ACCESS) {
return@accessRule UnAuthorizedUserException()
}
if (!validToken.user.roles.map { role -> forRoles.contains(role) }.reduce { acc, b -> acc && b }) {
return@accessRule InvalidActionForRoleException()
}
null
}
class InvalidActionForRoleException : Exception()
class UnAuthorizedUserException : Exception()
In KGraphQL
, each Query
or Mutation
uses a DSL, QueryDSL
or MutationDSL
. Both inherit from AbstractOperationDSL
which gives us a place to define access rules by providing context. Our user is already passed in this context. So I ended up creating this authenticate
method which checks if the user is logged in as well as having one or more roles that are in the forRoles
list.
Then we can use authenticate
inside authenticatedMutation
and authenticatedQuery
to check the user first before executing init
which gives us the rest of the mutation stuff like resolver
.
Finally we can define our authenticated queries similar to this:
val schema = KGraphQL.schema {
// Define your types here
type<User>()
mutation("login") {
resolver { username: String, password: String ->
// Do your login logic
// returns User
}
}
authenticatedQuery("getUsers", listOf(UserType.ADMIN)) { isActive: Boolean ->
// Do your get users logic
// returns List<User>
}
}
Formatting Exceptions
That last thing that remained is handling exceptions and formatting them in a way that a GraphQL client would handle. I think it’s fair to say this differs based on the client your are using but for me, I needed the response to be something like this:
{
"graphQLErrors": [
{
"message": "Something went wrong..."
}
]
}
Which means I needed a data class similar to this :
data class GraphQLError(val graphQLErrors: List<Map<String, String>>)
and to handle it inside my Ktor route, I wrapped the schema execution around a try/catch block to be able to parse exceptions to the format I wanted before sending the response.
authenticate(optional = true) {
route("/graphql") {
post {
try {
// execute request and schema
} catch (exception: Exception) {
call.respond(GraphQLError(listOf(mapOf("message" to e.localizedMessage))))
}
}
}
}
Hopefully this blog gave some ideas on how to build a GraphQL API with Ktor in Kotlin. We now have a full functioning GraphQL API with JWT authentication as well.
Special thanks to the maintainers of KGraphQL, Annyce Davis for this post and maintainers of the best docs I’ve seen, the GraphQL learn docs.