Development Web

Quickly Prototyping a Ktor HTTP API

August 18, 2022
Quickly Prototyping a Ktor HTTP API

Step 1 — Use the Ktor project gen­er­a­tor #

With the Ktor project gen­er­a­tor, you can quick­ly gen­er­ate a Ktor project with min­i­mal con­fig­u­ra­tion. You can either gen­er­ate it via Jet­brains’ web­site here Gen­er­ate Ktor Project or use IntelliJ’s built-in project gen­er­a­tor. For the sake of this tuto­r­i­al I used the web generator.

Once you’re on the set­tings set­up page, feel free to con­fig­ure it how you’d like or set it up with the defaults. I used this configuration: 

Next, add your plu­g­ins, and for a bare bones REST API, I like to choose:

  • Rout­ing — For pro­vid­ing named routes with HTTP methods
  • Con­tent­Ne­go­ti­a­tion & kotlinx.serialization — For serializing/​deserializing JSON con­tent accept­ed and returned via our routes.
  • Call Log­ging — A nice to have to add on some addi­tion­al log­ging for requests that make it to our server.

You’ll then be able to down­load the project as a zip file, unzip it and open the project in your IDE (Intel­liJ was used for this tuto­r­i­al, as it han­dles the gra­dle sync­ing and build­ing for us) and we’ll move onto the next step.

Step 2 — Set­ting up your routes #

You’ll notice that the project has a Application.kt file that sets up the serv­er and con­fig­ures plu­g­ins that may act as inter­cep­tors or pro­vide addi­tion­al func­tion­al­i­ty like rout­ing. There is also the plugins fold­er which con­tains the Monitoring.kt plu­g­in for Call Log­ging, Routing.kt for our end­points, and Serialization.kt that tells the serv­er how to under­stand JSON serialization/​deserialization. Each plu­g­in typ­i­cal­ly gets installed via Ktor’s DSL (Domain Spe­cif­ic Lan­guage) using the install() func­tion. The excep­tion here is that rout­ing is set­up with­in the con­text of Application by call­ing routing {}.

The Routing.kt plu­g­in starts out with these contents: 

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

Where it defines a sin­gle end­point accessed via a GET request to the base route / and responds with the text Hello World!.

You can run the serv­er by right-click on the Application.kt file and click­ing Run 'Application.kt'.

What we’ll do next is mod­i­fy this file to add on some addi­tion­al routes using that same DSL, as well as show how to sep­a­rate out the routes into sep­a­rate ​“mod­ules”.

Below is the updat­ed 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 exam­ple, we cre­at­ed a basic data class that was marked with kotlinx.serialization’s @Serializable anno­ta­tion to make it seri­al­iz­able, and the ContentNegotiation plu­g­in we installed will tell the serv­er that when­ev­er we send or receive an object of this type, to con­vert it to/​from JSON back into this class.

We then just keep a list of Books with­in mem­o­ry to act as our ​“data­base” for now and cre­ate two mod­ules, one for the /books end­point with fun Route.books() {} and anoth­er for the /authors end­point with fun Route.authors() {} and run these two func­tions with­in the con­fig­ureRout­ing block with a routing wrap­per, which comes from the Routing plu­g­in we installed. This gets ran with­in our Application.kt to tell the serv­er what routes we want to expose.

The meat of the rout­ing file involves the DSL functions:

  • route() {} — Defines a pre­fix­ing path to be used by our child routes called inside our function.
  • get() {} — Defines a GET end­point that can option­al­ly take para­me­ters with the syn­tax "{name?}" where name is the exposed vari­able of the path para­me­ter. You should call call.respond() and pass in an option­al sta­tus code and object, whose type is inferred, to respond to the client.
  • post() {} — When receiv­ing 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 sim­i­lar­ly to the func­tions above, but will han­dle DELETE methods.

Sum­ma­riz­ing the above, this is a great way to get a quick HTTP API up and run­ning and Ktor sup­ports even more func­tion­al­i­ty on top of this like: Type-safe rout­ing, authen­ti­ca­tion, Web­Sock­ets, and more. Feel free to check out the doc­u­men­ta­tion for Ktor.

Use­ful plu­g­ins to con­sid­er adding in lat­er #

The below plu­g­ins weren’t used for this tuto­r­i­al, but if I were to con­tin­ue pro­to­typ­ing the project, these are the plu­g­ins I’d opt for by default:

  • Caching­Head­ers
  • Authen­ti­ca­tion
  • CORS
  • Ses­sions

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

MichiganLabs’ approach to software delivery: 3 ways delivery leads provide value
Process Team

MichiganLabs’ approach to software delivery: 3 ways delivery leads provide value

February 12, 2024

Delivery leads ensure the successful execution of custom software development. They build great teams, provide excellent service to clients, and help MichiganLabs grow. Learn what you can expect when working with us!

Read more
How brain-computer-interfaces could improve human health
Business

How brain-computer-interfaces could improve human health

May 13, 2024

Read more
MichiganLabs’ approach to product design: A strategic, problem-solving process
Design Team

MichiganLabs’ approach to product design: A strategic, problem-solving process

February 12, 2024

Product design, or UX design, is a strategic problem-solving process that leads to a valuable digital product. Learn what to expect when working with product designers for your custom software.

Read more
View more articles