An opinionated Kotlin backend service - API Routing & Documentation

Emanuel Moecklin
Nerd For Tech
Published in
5 min readApr 30, 2021

This is the third part of my series on Kotlin based backend services.
If you missed the first two parts:

API Routing and documentation

Ktor Routing

Defining routes in Ktor is simple and very concise due to the DSL. To define a route, call the routing function and add the routes you need:

routing {
// here go your routes
}

Using different http methods:

routing {     
get("/customer/{id}") { /* handler */ }
post("/customer") { /* handler */ }
put("/customer/{id}") { /* handler */ }
}

Grouped by path:

routing {
route("/customer") {
get("/{id}") { /* handler */ }
post("/") { /* handler */ }
}
route("/account") {
get("/{id}") { /* handler */ }
}
}

Hierarchical:

routing {
route("/customer") {
route("/order") {
get("/{id}") { /* handler */ }
delete("/{id}") { /* handler */ }
}
}
}

Modular (using extension functions):

routing {
customerByIdRoute()
createCustomerRoute()
orderByIdRoute()
}
fun Route.customerByIdRoute() {
get("/customer/{id}") { /* handler */ }
}

fun Route.createCustomerRoute() {
post("/customer") { /* handler */ }
}

fun Route.orderByIdRoute() {
get("/order/{id}") { /* handler */ }
}

I don’t want to elaborate on the standard Ktor routing more in-depth because I’m actually not using it. If you’re interested to learn more about the regular Ktor routing read one of the numerous articles e.g. here, here or here.

OpenAPI Generation

The reason I’m not using the normal Ktor routing function is explained in this article: OpenAPI Generation with Ktor.

The gist is that I wanted to have an OpenAPI documentation and I decided to write code and use a generator to create the documentation automatically (as opposed to writing documentation and generating the code automatically). The code I write looks like this:

Definitions like the one above are used to:

  1. define the application routes
  2. and generate the OpenAPI documentation:

Route Definition

Let’s go through the definition step by step.

tag(Tags.Account) {
route("/api/v1/admin/accounts") {
  • The tag function is used to group routes together (in this case into groups like Misc, Customer and Account).
  • The route function is used to define the routes matching a specific path.

You can also create hierarchies of routes:

route("/api/v1") {
route("/misc") { }
route("/accounts") { }
route("/customers") { }
}

The actual routes are defined within the route function:

/**
* Create a new account.
*/
post<Unit, ResponseAccount, RequestAccount>(
info(
summary = "Create an account.",
description = "Create a new account"
),
status(HttpStatusCode.OK),
exceptions = listOf(badRequest),
exampleRequest = accountExampleRequest,
exampleResponse = accountExampleResponse
) { _, account ->
val newAccount = controller.createAccount(account)
respond(newAccount)
}

The two or three generic types (two for a get: get<AccountUUIDParam, ResponseAccount> three for a post: post<Unit, ResponseAccount, RequestAccount>) define the types for

  1. path parameters (e.g. path/{accountUUID})
  2. response
  3. request body (post, put)

The definition of these types looks like this (path parameter):

@Path("{accountUUID}")
data class AccountUUIDParam(
@PathParam("UUID of the account.")
val accountUUID: String
)

or (response):

@Response("Account Response.")
data class Account(
val accountUUID: String,
val createdAt: Instant,
val modifiedAt: Instant,
val status: AccountStatus
)

These data classes are used to:

  • Create the OpenAPI documentation:
  • Define the result of serialization/deserialization of requests/responses (to and from Json / data classes)
  • Define object validation rules. Further down I will elaborate on object validation in more detail.

Method Parameters

The method parameters for get, post, put, patch, delete and head functions are almost exclusively used to define the OpenAPI documentation (the exceptions parameters also tie into the object validation).

info(
summary = "Create an account.",
description = "Create a new account"
),
status(HttpStatusCode.OK),
exceptions = listOf(badRequest),
exampleRequest = accountExampleRequest,
exampleResponse = accountExampleResponse
  • The purpose of the info and status parameters is obvious (describe the route and the response status code). They are both RouteOpenAPIModule vararg parameters of which there are two more:
    1) resonseAnnotationStatusCode: the response HTTP status code is derived from the response definition, e.g:
    @Response("Account Response.", 202) -> overrides the status(HttpStatusCode.OK) statement
    2) tags: sets the tag for this route, if multiple tags are set, the route will appear in all the categories / groups.
  • The exceptions parameter defines possible error cases.

E.g. the badRequest definition:

translates to:

Note that the runtime error defined with contentFn depends on the exception message which is taken from the ConstraintViolation thrown as part of the validation process (see “Object Validation” chapter below).

  • Last but not least the exampleRequest and exampleResponse parameters which should be self explaining.

Object validation

I’ve written a dedicated article about object validation: Object validation in Ktor/Kotlin. In this article I researched several validation libraries like Kalidation, Valiktor or Konform. While I declared Konform my favorite, I actually don’t use it because the Ktor-OpenAPI-Generator has a built-in validation mechanism. While I’m not a particular fan of annotation based validation (I like to use a single language for everything including configuration and build, that single language being Kotlin here), the advantage in this case is that it serves two purposes:

  1. Validation
  2. Documentation

E.g. the annotations here are used by the Ktor-OpenAPI-Generator to validate the input parameters and also to create the documentation:

Error Handling

What happens if an input parameter or the request body fails validation?

Ktor-OpenAPI-Generator throws a ConstraintViolation which is handled by the badRequest definition posted above:

val badRequest = apiException<ConstraintViolation, ErrorMessage> {
// more code here
}

which in turns is used in the route definition (also posted above) and we come full circle:

post<Unit, ResponseAccount, RequestAccount>(
...
exceptions = listOf(badRequest),
...
) {
...
}

That’s it for part 3 of the series. If you enjoyed this, follow up with An opinionated Kotlin backend service — Database / Migration / ORM.

As usual feel free to provide feedback. Happy coding!

--

--

Nerd For Tech
Nerd For Tech

Published in Nerd For Tech

NFT is an Educational Media House. Our mission is to bring the invaluable knowledge and experiences of experts from all over the world to the novice. To know more about us, visit https://www.nerdfortech.org/.

No responses yet