Server Side Kotlin - How I structure web services in Kotlin
A look into how I like to develop web APIs in Kotlin that can be used in any web framework such as Ktor, Spark, Vert.x, or Spring
Over the past few years I have worked on a number of server side Kotlin projects. These HTTP services that have supported web apps, mobile apps, and kiosks. I have learned a lot about using Kotlin to build APIs and I would like to share some of the patterns I am using today to build HTTP services in Kotlin.
These patterns can be utilized in any framework such as Micronaut, Ktor, Spark, Vert.x, and Spring and they also help isolate business logic, map that logic cleanly to HTTP responses, and ensure code can be easily tested.
Code Organization
All projects start with the standard Gradle project structure. I like to separate out each deployable JAR into its own module. I also create modules prefixed with lib- to identify my libraries. Below is a sample project structure that contains an API server and an identity module that handles authentication with email and password.
project-root/
api-server/ - This is where my web services code lives
lib-identity/
src/main/kotlin/
EmailIdentityApi.kt
EmailIdentityData.kt - This is where implementations go
Inside my library modules I create a single file that ends with Api. This file contains the only classes you need to understand in order to interact with the module. Below is an example of what you might find in this file.
data class EmailIdentity(val id: UUID,
val userId: UUID,
val email: String)
sealed class EmailIdentityResult {
data class Success(val identity: EmailIdentity?) : EmailIdentityResult()
object InvalidCredentials : EmailIdentityResult()
object NotFound : EmailIdentityResult()
}
interface EmailIdentityApi {
fun authenticate(email: String, password: String): EmailIdentityResult
fun changePassword(userId: UUID, newPassword: String): EmailIdentityResult
fun get(userId: UUID): EmailIdentityResult
}
In this file I define my domain objects, service interface for interacting with those objects, and a sealed class that represents the complete set of potential outcomes. This has a number of benefits.
- You only have to look inside a single file to understand what the module has and how to interact with it
- The Result encapsulates all success and error states.- When someone changes the Result class the compiler can indicate where adjustments might be needed
- You can mark every other declaration in the module as internal or private and not worry about things leaking out of the module
Feature Implementation
With the module above I can expose an authentication endpoint via any web framework. I start by creating an Endpoint class that simply parses inbound request payloads and responds appropriately.
class AuthEndpoint @Inject constructor(
vertx: Vertx,
jwtAuthHandler: JWTAuthHandler,
jwtTokenFactory: JwtTokenFactory,
emailIdentityApi: EmailIdentityApi,
dispatcher: ApiDispatcher) : Endpoint(vertx) {
init {
val controller = EmailController(jwtTokenFactory,
emailIdentityApi,
companyApi,
dispatcher)
router.post("/auth").respond { ctx ->
controller.authenticate(ctx.request().params()["email"],
ctx.request().params()["password"])
}
}
}
In this example I am using the Vert.x web framework but you can use this same pattern with Micronaut, Ktor, Spark, or Spring. I like using dependency injection to provide implementations of interfaces I need via constructors which also aids in simpler unit and integration testing.
In the same file as the endpoint I create a controller class that handles the business logic. The endpoint defers all work to its controller. This allows for unit and integration testing of the endpoint and controller within the same module.
internal class EmailController(private val jwtTokenFactory: JwtTokenFactory,
private val emailIdentityApi: EmailIdentityApi,
private val dispatcher: ApiDispatcher) {
suspend fun authenticate(email: String, password: String): HttpResponse {
return withContext(dispatcher.io()) {
when (val result = emailIdentityApi.authenticate(email, password)) {
is EmailIdentityResult.Success -> {
val token = jwtTokenFactory.createToken(result.identity!!.userId.toString())
HttpResponse.Ok(mapOf("token" to token))
}
is EmailIdentityResult.InvalidCredentials -> {
HttpResponse.Error(401, "Invalid credentials")
}
is EmailIdentityResult.NotFound -> {
HttpResponse.Error(401, "Invalid credentials")
}
else -> {
HttpResponse.Error(500, "Unable to authenticate")
}
}
}
}
}
In this controller we utilize our EmailIdentityApi defined in our lib-identity module. We call our authenticate method and in return we get a sealed class. With that sealed class we then iterate over all the potential scenarios using the when statement - this coverage is enforced by the compiler. This is a powerful style of programming akin to Pattern Matching and Railway Oriented Programming which is popular in many other languages.
Finally from the controller we return another sealed class. This sealed class lets controllers map business outcomes to HTTP responses.
sealed class HttpResponse {
data class Ok(val data: Any?) : HttpResponse()
data class Error(val status: Int, val message: String, val exception: Throwable? = null) : HttpResponse()
data class Errors(val status: Int, val errors: List<String>, val exception: Throwable? = null) : HttpResponse()
}
Testing
With these implementations it is simple to instantiate and test. In order to unit test I provide mocked objects and verify the high level logic. Once the unit tests pass - instantiating them with real objects allows for full end to end integration testing.
Conclusion
The patterns above have helped me structure flexible and performant web services in Kotlin. If you have any questions or comments please reach out to me on Mastodon or Twitter.