Europe Union
Published: 05/09/2023

Kotlin, Ktor, Arrow Core – basic server applications with API and error handling implementations

In this article, we will explore how to create a standalone server application in Kotlin with Arrow libraries using Ktor. To validate and test the whole application, we are going to use Kotest. The context of our usage of this application is to bake my favorite cakes.

Kotlin is a modern but already mature programming language aimed at making developers happier. It’s concise, safe, interoperable with Java and other languages, and provides many ways to reuse code between multiple platforms for productive programming.

Ktor is a framework for building asynchronous servers and clients in connected systems using the powerful Kotlin programming language. It facilitates the development of a standalone application with embedded servers.

Arrow is a functional companion to Kotlin’s Standard Library. It is divided into four modules

  • Core – includes types such as Either, Validated and many extensions to Iterable that can be used when implementing error handling patterns. Core also includes the base continuation effects system, which includes patterns to remove callbacks and enables controlled effects in direct syntax. Some applications of the effect system reduce boilerplate and enable direct syntax, including monad comprehensions and computation expressions.
  • FX – a full-featured, high-performance, asynchronous framework that brings functional operators to Kotlin’s suspend functions.
  • Optics – automatic DSL that allows users to use .(dot) notation when accessing, composing, and transforming deeply nested immutable data structures.
  • Meta – general purpose library for meta-programming in Kotlin to build compiler plugins.

Kotest is a flexible and elegant multi-platform test framework for Kotlin with extensive assertions and integrated property testing

Before going further, let’s present some advantages Arrow gives and uses in this article.

Advantages of working with Arrow typed errors

Working with typed errors offers a few advantages over using exceptions:

  • Type Safety: Typed errors allow the compiler to find type mismatches early, making it easier to catch bugs before they make it to production. However, with exceptions, the type information is lost, making it more difficult to detect errors at compile-time.
  • Predictability: When using typed errors, the possible error conditions are explicitly listed in the type signature of a function. This makes it easier to understand the possible error conditions and write tests covering all error scenarios.
  • Composability: Typed errors can be easily combined and propagated through a series of function calls, making writing modular, composable code easier. With exceptions, ensuring errors are correctly propagated through a complex codebase can be difficult.
  • Performance: Exception handling can significantly impact performance, especially in languages that don’t have a dedicated stack for exceptions. Typed errors can be handled more efficiently as the compiler has more information about the possible error conditions.

In summary, typed errors provide a more structured, predictable, and efficient way of handling errors and make writing high-quality, maintainable code easier.

Glosary

Either is a class Either<A, B> from Arrow, which has two generic parameters for the type of the two values. They are denoted as Right and Left. This class is designed to be right-biased. So, the right branch should contain the business value, the result of some computation. The left branch can hold an error message or even an exception.

Either.bind() folds Either<A, B> into Effect by returning right or a shift with the left on any error. So it is resolving Either to B object on success or to A on any failure. Left A will be passed to the higher layer of code, and our application can recover from failure or break processing operation from the current Effect scope.

Either.catch() is a method which allows us to wrap foreign functions and capture any Throwable or T: Throwable that might be thrown. It requires two functions, or lambdas, as arguments: One for wrapping our foreign code and another for resolving the captured Throwable or T: Throwable. In this case.

Either.Left() is the constructor of Either<A, Nothing>

ensure() is a method from Either DSL that is checking given prerequisites. It returns Left if the passed condition is unsatisfied and breaks the current Effect scope.

Setting up application

Let’s start by setting up the Koltin project with Ktor and Arrow. We’ll be using the Gradle build tool. The main libraries are presented below.

dependencies {
	implementation("io.arrow-kt:arrow-core:1.1.5")
	implementation("io.ktor:ktor-server-netty:2.2.4")
}

We’ve imported Ktor and the Ktor netty server package. The whole application will be implemented with an arrow-core module which provides Either and some useful DSL for error and validation handling. It should improve the safety and readability of our code.

Building the Server and Set Routing

We create our application by adding code to the source folder src/main/kotlin.

Here, we create the file App.kt with the main method:

fun main() {
	embeddedServer(Netty, port = 8080) {
    	install(Routing) {
        	cakeRoutes(CakeService())
    	}
	}.start(wait = true)
}

We create and start the server at port 8080. We have set wait=true in the start() method to listen for connections. Also, we installed Routing, which we exposed as HTTP endpoints. Routing is one of the Ktor plugins which allow us to route HTTP requests on given endpoints to our methods in code.

In this example, the server will handle a POST request for the path /cakes, taking a CakeRequest as Body in JSON. It will reply with a CakeResponse JSON. Implementation will be more described in the next sections of this article.

Building the API

Now we create an API which will be described under the cakeRoutes() method. CakeRequest and CakeResponse data classes have Serializable annotation to easily map JSON to Object and Object to JSON during processing requests. It will be described below.

@Serializable
data class CakeRequest(
	val cakeType: CakeType,
	val cakeName: String,
)

@Serializable
data class CakeResponse(
	val cakeId: Int,
	val message: String
)

enum class CakeType {
	CHEESE_CAKE,
	CHOCO_CAKE,
}

fun Route.cakeRoutes(cakeService: CakeService) {
	safePost("/cakes") { call ->
    	val request: CakeRequest = call.safeReceive().bind()
    	cakeService.bakeCake(request.cakeType)
	}
}

Here, we have a straightforward way to create any endpoint. cakeRoutes() store all used cake endpoints. In the following example, we have only one POST operation. We can also add other operations (GET, PUT, DELETE, etc.). safePost() is our method that has implemented error handling and wraps post() from Ktor, and it is used to define our POST operation. As a param, we pass an endpoint path, /cakes. We also provide a handler body that will be run under this endpoint. Inside the handler, we receive a JSON request body and deserialize it to a CakeRequest model using safeRecsendingeive() this method will be explained later). Next, we call the service method bakeCake() with this request and get back a CakeResponse. Under safePost(), we take CakeResponse from the last operation, serializing it to JSON and  it back with the appropriate HTTP Status Code 201 (Created). Response Code is resolved by our method toApiResponse() described later. On any error, it will return HTTP Error Status Code from 40x or 50x. 

For serialization and deserialization, we use the kotlinx.serialization library. It provides two methods Json.decodeFromString() and Json.encodeToString(). It is a beneficial and easy way to convert String to Object and vice versa. Both of them are throw 

SerializationException and IllegalArgumentException. We catch and map them using Either.catch to our domain error types described by the sealed class ApiError.

Adding more controllers

Now we create some more examples of endpoints like GET, PUT, and DELETE. These endpoints need to get cakeId from the path. It could cause some exceptions during getting or converting. We will implement retrieving the parameter from the path using the arrow EitherScope to define possible errors. Below is one example of how to handle the path param using the extension method – ApplicationCall.getIdPathParam(). We will pass a path parameter name. To simplify, we catch all possible errors and transform them into our InvalidRequest.

fun ApplicationCall.getIdPathParam(
	paramName: String,
): Either<ApiError, Int> {
	val rawValue = this.parameters[paramName]

	return if (rawValue.isNullOrBlank()) {
    	Either.Left(InvalidRequest("Path parameter $paramName was not provided"))
	} else {
    	Either.catch {
        	rawValue.toInt()
    	}.mapLeft {
        	InvalidRequest(
            	"Path parameter $paramName value $rawValue can not be converted to Int",
            	cause = it
        	)
    	}
	}
}

First, we will try to get an Id param from the path using paramName passed as a method argument. If this param is not found, then we will return left with InvalidRequest. When the param is found, we will try to convert the param’s value to Int. As a result, we will have a wanted param’s value parsed to Int. If converting will raise any error, it will be caught by Either.catch and mapped to InvalidRequest with the appropriate message. Then it will be returned as Either.left or resolved in the future.

By using this approach, we can create some new HTTP operations like those below.

safeGet("/cakes") {
    	cakeService.getCakes()
	}

	safePut("/cakes/{cakeId}") { call ->
    	val cakeId = call.getIdPathParam("cakeId").bind()
    	val request: CakeRequest = call.safeReceive().bind()
    	cakeService.updateCake(cakeId, request)
	}

	safeDelete("/cakes/{cakeId}") { call ->
    	val cakeId = call.getIdPathParam("cakeId").bind()
    	cakeService.deleteCake(cakeId)
	}

This approach gives a very easy-to-understand, safe, and extendable way to implement our API. All these controllers will be applied to our application by extension function Route.cakeRoutes() by installing the Ktor Routing plugin described before.

Validation and error handling

Now let’s explain more about how validations and errors are handled in the application. For example, our safePost() method (wrapper for Ktor post())is covered by error handling using EffectScope and Either DSL. All operations can lead to a specific ApiError, a sealed class containing all possible error types in our application. In the following example, we recognise a few possible errors like CakeTypeNotSupported, InternalServerError, and InvalidRequest. They all store messages and causes to explain to the Client or in logs what happened easily. Causes can be nullable as we can create an error in EffectScope not only by catching some exceptions but also with Either.left() when validating data. 

sealed class ApiError
{
	abstract val message: String
	abstract val cause: Throwable?
}

data class CakeTypeNotSupported(
	override val message: String,
	override val cause: Throwable? = null,
) : ApiError()

data class InternalServerError(
	override val message: String,
	override val cause: Throwable? = null,
) : ApiError()

data class InvalidRequest(
	override val message: String,
	override val cause: Throwable? = null,
) : ApiError()

All EffectScope methods are finalized by monad .bind() which terminates current and next operations in case of error and passes it to the higher level of application (parent method). Then we can pass this error to a higher level or try to resolve it. In the end, unresolved errors are translated to specific HTTP error status codes and sent to the client with precise messages. To see how to do it, let’s go to a lower level of our API utils, which wrap the Ktor implementation.

@KtorDsl
inline fun <reified T : Any> Route.safePost(
	path: String,
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
): Route =
	safeRoute(path = path, method = HttpMethod.Post, handler = handler)

@KtorDsl
inline fun <reified T : Any> Route.safeGet(
	path: String,
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
): Route =
	safeRoute(path = path, method = HttpMethod.Get, handler = handler)

@KtorDsl
inline fun <reified T : Any> Route.safePut(
	path: String,
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
): Route =
	safeRoute(path = path, method = HttpMethod.Put, handler = handler)

@KtorDsl
inline fun <reified T : Any> Route.safeDelete(
	path: String,
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
): Route =
	safeRoute(path = path, method = HttpMethod.Delete, handler = handler)

@PublishedApi
internal inline fun <reified T : Any> Route.safeRoute(
	path: String,
	method: HttpMethod,
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
): Route =
	route(path, method) {
    	handle {
        	call.handleSecuredRequest(handler)
    	}
	}

@PublishedApi
internal suspend inline fun <reified T : Any> ApplicationCall.handleSecuredRequest(
	crossinline handler: suspend EffectScope<ApiError>.(ApplicationCall) -> Either<ApiError, T>
) {
	val response: ApiResponse = either {
    	Either.catch {
        	val responseFromHandler: T = handler(this@handleSecuredRequest).bind()
        	Json.encodeToString(responseFromHandler)
    	}.mapLeft {
        	InternalServerError(
            	message = "Unexpected error occur when handling your request",
            	cause = it
        	)
    	}.bind()
	}.toApiResponse()
	respond(response.status, response.message)
}

So safePost() only calls the more generic method safeRoute() which takes the path, POST request method and operation handler as params. Both of them return the current Route with the appropriate response body and code.

safeRoute() calls call.handleSecuredRequest(). Under this method, we execute all wanted operations, map response Objects to JSON and set an appropriate HTTP response code. This method also uses EffectScope and resolves any errors. It takes care of any unexpected throws using Either.catch and mapping them to InternalServerError. Errors are mapped inside .toApiResponse() method, where the HTTP status code is resolved and a response body is created.

sealed class ApiResponse {
	abstract val status: HttpStatusCode
	abstract val message: String
}

data class ErrorResponse(
	override val status: HttpStatusCode,
	override val message: String,
) : ApiResponse()

data class BodyResponse(
	override val status: HttpStatusCode,
	override val message: String,
) : ApiResponse()

fun Either<ApiError, String>.toApiResponse(
	onLeft: (ApiError) -> ApiResponse = { it.resolveLeft() },
	onRight: (String) -> ApiResponse = { BodyResponse(HttpStatusCode.Created, it) }
): ApiResponse =
	fold(
    	ifLeft = onLeft,
    	ifRight = onRight,
	)

fun ApiError.resolveLeft(): ApiResponse {
	val responseCode: HttpStatusCode =
    	when (this) {
        	is CakeTypeNotSupported -> HttpStatusCode.NotAcceptable
        	is InternalServerError -> HttpStatusCode.InternalServerError
        	is InvalidRequest -> HttpStatusCode.BadRequest
    	}
	return ErrorResponse(responseCode, message)
}

.toApiResponse() method takes two methods onLeft and onRight, as parameters. By using fold(), Either<ApiError, String> will be resolved, and ApiResponse will be returned, potentially free from any exceptions. Any Either.left will be resolved to ErrorResponse using ApiError.resolveLeft(). Any Either.right means correctly returned value that will be mapped to BodyResponse with 201 HTTP status code. We can see that:

  • CakeTypeNotSupported will return 406 – Not acceptable 
  • InvalidRequest will return 400 – Bad request
  • InternalServerError will return 500 – Internal server error. 

Bodies of all possible returns would be returned, too (in the message param in the ErrorResponse object).

In the end, the Ktor respond(response.status, response.message) method sends back a response with a given HTTP status code and response body.

One place in which an exception can be thrown is inside the safeReceive() method. During resolving the request body, kotlinx serializer can throw a SerializationException or IllegalArgumentException. We would like to catch them and map them to one of our ApiError to set them with the precise HTTP response status code and resolve them properly.

suspend fun ApplicationCall.safeReceive(): Either<InvalidRequest, CakeRequest> =
	Either.catch {
    	Json.decodeFromString<CakeRequest>(receive())
	}.mapLeft {
    	InvalidRequest(message = "Failed to receive object of type CakeRequest", cause = it)
	}

safeReceive() is wrapped by Either.catch which will catch all exceptions. By using mapLeft, the given exception will be mapped to our InvalidRequest. All next operations will be skipped because we used monad .bind() on the current EffectScope. The .toApiResponse() method will set the appropriate HTTP response status code 400 (Bad request) to this error and send it back according to Ktor respond().

Many different errors could happen during request processing. We can extend ApiError

as much as we need. Based on the error type, we can decide whether to handle it or send back information to the client that something went wrong.

In the following example, we will also throw some exceptions from the Service layer to show more possibilities of our approach and pay some attention to implementation issues.

We can implement the same mechanism for business validations. All checks would return specific error types over which we have full control. All validation problems would be returned as Either.left. If validation passes, then the value will be returned as an Either.right (usually just Unit). Below is an example of how validation in one service method processBaking() could lead to an error and break all further application logic.

	private suspend fun processBaking(cakeType: CakeType): Either<ApiError, Int> =
    	either {
        	ensure(cakeType == CakeType.CHEESE_CAKE) {
            	CakeTypeNotSupported("Only Cheese cake can be served")
        	}
        	mixIngredients().bind()
        	cook().bind()
        	Random.nextInt()
    	}

As we see, only CHEESE_CAKE is supported now. Any other cake type would raise a CakeTypeNotSupported error. If validation fails, Either.left will be sent back to a higher level of our application, and all further logic will be stopped. Ingredients won’t be mixed, and the cake will not be cooked. Then, we will resolve the returned CakeTypeNotSupported error to the 406 HTTP error status according to our method resolveLeft(), for ApiResponse.

Running the Server

To run the server, we need a run task in Gradle:

application {
	mainClass.set("AppKt")
}

To start the server, we call this task:

./gradlew run

Our API can then be accessed via http://localhost:8080/.

Potential hiccups with the current implementation

We implemented a safe, self-explanatory server application with an error-handling mechanism on every layer. As always, something can go wrong if we are unaware of some limitations.

For example, monad .bind() could be a potential issue with the wrong implementation. Sometimes EffectScope can return an exception on the left and a Unit on the right like Either<Throwable, Unit>. In this case, we are not supposed to have any return Object from that method, so we don’t assign them to any values. We just apply .bind() to pass errors to a higher level. If we miss .bind(), everything will still be correct for the compiler.

Let’s take a look at the example below.

	private suspend fun processBaking(cakeType: CakeType): Either<ApiError, String> =
    	either {
        	mixIngredients()
        	cook().bind()
    	}

We see that processBaking() is returning Either<ApiError, String>. Everything would be fine, but if mixIngredients() also returns Either<ApiError, Unit> and we will not finalize it using .bind(), then in case of an issue, we will not break processBaking() and error from mixIngredients() will not even be noticed. If cook() returns a value without any error, then we will get response status code 201 (Created), which is, of course, wrong as we haven’t mixed our ingredients properly!

To prevent this from happening, we should write some unit tests for known error cases to see if we properly catch them. We also support using monad .bind() in the static code analyzer for Kotlin Detekt. It will point out the missing bind() during the code scan. It is strongly recommended to do so, as in another way, our reviewer would need to check it each time, or we would have a tricky bug raised later.

Another thing that can happen is to forget about catching possible exceptions with Either.catch(). In our example, the highest level will catch it inside the following code.

Either.catch {
        	val responseFromHandler: T = handler(this@handleSecuredRequest).bind()
        	Json.encodeToString(responseFromHandler)
    	}.mapLeft {
        	InternalServerError(
            	message = "Unexpected error occur when handling your request",
            	cause = it
        	)
    	}.bind()

We wrapped the execution of our endpoint handler with Either.catch(). HTTP status code 500 will be returned to the client on every exception not caught by our code. As we know, the 500 code is an internal server error. All potential exceptions should be caught and resolved during implementation, as it is a potential issue we would need to fix later.

Conclusion

Our example presented the basic usage of Kotlin, Arrow and Ktor libraries together. Ktor covered the server side of the application, and Arrow helped with error handling. We put an effort into best coding practices, readability of code and safety.

This approach which this article covered could be further enriched by much more complex mechanisms for authentication, database connection, log handlers, configuration, deep test ccoverage,etc., but these topics will be described deeply in separate articles. We hope you enjoy this article and that it helps with your implementation. Good luck! And if you’d like some assistance, don’t hesitate to contact us for our expertise.

References

Join the Kotlin Crew

Kotlin Crew is the fastest-growing Kotlin community! It was created by DAC.digital to establish a vibrant and inclusive place for Kotlin enthusiasts to network, exchange knowledge, and learn.

If you are looking for real-life Kotlin use-cases, insights about language features, and integrations with other technologies – take your seat because you are in the right place. 

What can you find in the Kotlin Crew Community?

  • technical articles by developers who program in Kotlin on a daily basis,
  • video interviews with business owners and developers who use Kotlin in their projects and share their stories and insights with us,
  • links to articles on Kotlin-related news,
  • a space to ask questions, share ideas and seek new solutions.

Join the Kotlin Crew!

Learn more about Kotlin Crew Community!
ornament ornament

Estimate your project.

Just leave your email address and we’ll be in touch soon
ornament ornament