The 5 Minute Accessibility Strategy
May 18, 2023We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.
Read moreWith the Ktor project generator, you can quickly generate a Ktor project with minimal configuration. You can either generate it via Jetbrains’ website here Generate Ktor Project or use IntelliJ’s built-in project generator. For the sake of this tutorial I used the web generator.
Once you’re on the settings setup page, feel free to configure it how you’d like or set it up with the defaults. I used this configuration:
Next, add your plugins, and for a bare bones REST API, I like to choose:
You’ll then be able to download the project as a zip file, unzip it and open the project in your IDE (IntelliJ was used for this tutorial, as it handles the gradle syncing and building for us) and we’ll move onto the next step.
You’ll notice that the project has a Application.kt
file that sets up the server and configures plugins that may act as interceptors or provide additional functionality like routing. There is also the plugins
folder which contains the Monitoring.kt
plugin for Call Logging, Routing.kt
for our endpoints, and Serialization.kt
that tells the server how to understand JSON serialization/deserialization. Each plugin typically gets installed via Ktor’s DSL (Domain Specific Language) using the install()
function. The exception here is that routing is setup within the context of Application
by calling routing {}
.
The Routing.kt
plugin starts out with these contents:
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
Where it defines a single endpoint accessed via a GET
request to the base route /
and responds with the text Hello World!
.
You can run the server by right-click on the Application.kt
file and clicking Run 'Application.kt'
.
What we’ll do next is modify this file to add on some additional routes using that same DSL, as well as show how to separate out the routes into separate “modules”.
Below is the updated example:
@Serializable
data class Book(val name: String, val author: String)
val listOfBooks = mutableListOf<Book>()
fun Route.books() {
route("/books") {
get {
call.respond(listOfBooks)
}
get("{name?}") {
val name = call.parameters["name"] ?: return@get call.respondText(
"Missing book name",
status = HttpStatusCode.BadRequest
)
val filteredBooksByName = listOfBooks.filter { it.name == name }
call.respond(filteredBooksByName)
}
post {
val newBook = call.receive<Book>()
listOfBooks.add(newBook)
call.respond(HttpStatusCode.Created, newBook)
}
delete("{name?}") {
val name = call.parameters["name"] ?: return@delete call.respondText(
"Missing book name",
status = HttpStatusCode.BadRequest
)
// If any books were deleted
when (listOfBooks.removeIf { it.name.equals(name, ignoreCase = true) }) {
true -> call.respond(HttpStatusCode.Accepted)
false -> call.respond(HttpStatusCode.NotFound)
}
}
}
}
fun Route.authors() {
route("/authors") {
get {
call.respond(listOfBooks.map{ it.author }.distinct())
}
}
}
fun Application.configureRouting() {
routing {
books()
authors()
}
}
In the above example, we created a basic data class that was marked with kotlinx.serialization’s @Serializable
annotation to make it serializable, and the ContentNegotiation
plugin we installed will tell the server that whenever we send or receive an object of this type, to convert it to/from JSON back into this class.
We then just keep a list of Books within memory to act as our “database” for now and create two modules, one for the /books
endpoint with fun Route.books() {}
and another for the /authors
endpoint with fun Route.authors() {}
and run these two functions within the configureRouting block with a routing
wrapper, which comes from the Routing
plugin we installed. This gets ran within our Application.kt
to tell the server what routes we want to expose.
The meat of the routing file involves the DSL functions:
route() {}
— Defines a prefixing path to be used by our child routes called inside our function.get() {}
— Defines a GET endpoint that can optionally take parameters with the syntax "{name?}"
where name
is the exposed variable of the path parameter. You should call call.respond()
and pass in an optional status code and object, whose type is inferred, to respond to the client.post() {}
— When receiving a POST body, you can call call.receive
and define the type you’d like to parse from the client. Make sure to still call call.respond()
to let the client know that you received their request.delete() {}
— Behaves similarly to the functions above, but will handle DELETE methods.Summarizing the above, this is a great way to get a quick HTTP API up and running and Ktor supports even more functionality on top of this like: Type-safe routing, authentication, WebSockets, and more. Feel free to check out the documentation for Ktor.
The below plugins weren’t used for this tutorial, but if I were to continue prototyping the project, these are the plugins I’d opt for by default:
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.
Read moreLearn how to use Android's Jetpack Compose to navigate from a screen inside of one NavigationBarItem to another.
Read moreA look at Product Strategy at MichiganLabs. Why we do it, what it is, what it is not, and how we approach it.
Read more