Building web APIs in Kotlin using the Ktor framework
In this article, we’ll create a simple web API using Ktor a framework for building web applications in Kotlin. We’ll demonstrate how to create endpoints that consume and produce JSON requests/responses.
Check out the example on GitHub: minibuildsio/ktor-example.
Getting started
The easiest way to create a Ktor application is to use the Ktor Project Generator https://start.ktor.io/.
Add the following plugins:
- Routing: define endpoints to handle incoming requests.
- Content Negotiation: converts request body based on the content type header.
- kotlinx.serialization: serialize/deserialize JSON (and other formats if needed).
Add Features using Ktor Modules
Ktor uses modules to encapsulate specific features for example serialization and routing can be added to the application like so:
fun Application.module() {
configureSerialization()
configureRouting(PlaceService())
}
Content Negotiation and Serialization
The function below will install the ContentNegotiation
plugin and register the JSON serializer. Custom serialization modules can
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
To make a class serializable it needs to be annotated with @Serializable
e.g.
@Serializable
data class Place(
val id: Int,
val name: String,
val location: Location,
val type: PlaceType
)
Routing
The function below will install the Routing
plugin using the routing
function, it’s equivalent to install(Routing) { ... }
. Inside the routing
call, you can call get("/endpoint")
, post("/endpoint")
, etc to set up handlers for HTTP requests for particular endpoints. Inside the handler, the call
context provides access to the request query params, request body, and headers as well as the ability to set the response.
The get("/places")
handler gets the query parameter called name
and response with a list of places that match that name retrieved from the placesService
.
fun Application.configureRouting(placeService: PlaceService) {
routing {
get("/places") {
val name = call.request.queryParameters["name"]
call.respond(placeService.getPlaces(name))
}
}
// other routes...
}
Getting the request body
The request body can be accessed using the contexts receive function e.g. call.receive<...>()
, with the type of the request body being provided in the generic parameter.
post("/visits") {
val visit = call.receive<VisitRequest>()
call.respond(visitService.addVisit(visit.placeId, visit.visitDateTime))
}
Using path variables
Path variables are supported using curly braces e.g. “/places/{id}”. The value of the path variable can be accessed using call.parameters
e.g. call.parameters["id"]
.
get("/places/{id}") {
val id = call.parameters["id"]!!.toInt()
val place = placeService.getPlace(id)
if (place == null) {
call.respond(HttpStatusCode.NotFound)
} else {
call.respond(place)
}
}
Testing a Ktor Application
Ktor provides the testApplication
function to spin up an instance of the application. testApplication
creates a client to interact with the running application, this allows you to make requests to the application and make assertions on the response.
@Test
fun `get places returns places from the places service`() = testApplication {
client.get("/places").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(DEFAULT_PLACES, Json.decodeFromString(bodyAsText()))
}
}
Occasionally the default client won’t be configured to handle the response in that case the createClient
function can be used to build a customised client. Below we create a client that adds a serializer module to parse LocalDateTime
.
@Test
fun `post request to visits creates a visit`() = testApplication {
val client = createClient {
install(ContentNegotiation) {
json(Json {
serializersModule = SerializersModule {
contextual(LocalDateTimeSerializer)
}
})
}
}
client.post("/visits") {
contentType(ContentType.Application.Json)
setBody(VisitRequest(10, DATETIME))
}
client.get("/visits").apply {
val places: List<Visit> = body()
assertEquals(1, places.size)
assertEquals(10, places[0].placeId)
assertEquals(DATETIME, places[0].visitDateTime)
}
}