Building a backend, an API and a web client using Ktor

7 minute read

Note: Thanks Kotlin Weekly for featuring this article in Kotlin Weekly (#241) 🙏

Some time ago I had a potential client that wanted the following:

  • An Android app that would send data to a backend.
  • The app would also consume and display data from the backend.
  • The consumed data would be created by an administrator in a website (dashboard look-alike).

Pretty standard, right? But there was a small caveat: I specialise in Android development and I wasn’t sure if I could deliver the backend and web experience to the required standard.

With this in mind, I started looking into potential solutions before taking on the project. Ktor, an asynchronous framework for building backend, api and web applications, was an easy choice since it looked quite straightforward and it is based in Kotlin, a language that I’m very familiar with.

That’s how I decided to build FlightPlanner with Ktor, a small pet project to track booked flights with similar requirements to the client’s project: a backend application backed by a DB, an API to be used by mobile clients and a web client to visualise and add new data.

FlightPlanner

Before we dive into seeing some code, it’s worth mentioning that the project can be found in GitHub. Now, let’s get to it!

Ktor Features

Ktor heavily relies on the idea of Features. Features are, well, features that you want your backend application to have. They’re installed in the initialization phase and they’re a great way to build extensible lightweight applications. It looks like this:

fun Application.main() {
  install(Locations)
  install(DefaultHeader)
  install(Authentication) {
    jwt {
      //feature configuration
    }
  }
  ...
}

Building our first endpoint

One of the greatest Ktor features is Routing. Routing allows us to decide how the backend should handle a client request. For FlightPlanner, I wanted to have a /flights endpoint to fetch user’s flights.

First, we install the Routing feature in our Application.kt:

install(Routing) {
  flightsApi(FlightsRepository)
}

And our FlightsApi.kt:

const val FLIGHTS_API_ENDPOINT = "$API_VERSION/flights"

@Location(FLIGHTS_API_ENDPOINT)
class FlightsApi

fun Route.flightsApi(flightsRepository: FlightsRepository) {

  authenticate("jwt") {
    get<FlightsApi> {
      val user = call.apiUser
      val flights = flightsRepository.getFlights(user.userId)
      call.respond(flights)
    }

    post<FlightsApi> {
      val user = call.apiUser
      try {
        val flightApiRequest = call.receive<FlightApiRequest>()
        val flight = flightsRepository.add(flightApiRequest.toFlight(), user.userId)
        if (flight != null) {
          call.respond(flight)
        } else {
          call.respondText("Invalid data received", status = HttpStatusCode.InternalServerError)
        }
      } catch (e: Throwable) {
        call.respondText("Invalid data received", status = HttpStatusCode.BadRequest)
      }
    }
  }
}

Keen eyes might have spotted authenticate("jwt") and FlightsRepository, but let’s ignore these details for now since we’ll cover them later.

Doing a GET on /flights for an authenticated user will return something like this đź’Ą

[
    {
        "bookingReference": "DSG32",
        "departingDate": "Jan 2, 2020 10:00:00 AM",
        "arrivalDate": "Jan 1, 2020 2:00:00 PM",
        "origin": "London",
        "destination": "Granada",
        "airline": "Ryanair",
        "people": 2,
        "price": 125.00
    },
    {
        "bookingReference": "Z35DR",
        "departingDate": "Dec 21, 2020 7:30:00 AM",
        "arrivalDate": "Dec 21, 2020 11:45:00 AM",
        "origin": "London",
        "destination": "Bangkok",
        "airline": "Thai Airways",
        "people": 2,
        "price": 800.00
    },
  ...
]

Storing data into a DB

We want to store our users’s flights, meaning we need a database. Luckily, with Ktor we can use Exposed (ORM for Kotlin) and a PostgreSQL database for it.

The setup is relatively straightforward:

object DatabaseFactory {

  fun init() {
    Database.connect(hiraki())

    transaction {
      SchemaUtils.create(Flights)
      SchemaUtils.create(Users)
    }
  }

  private fun hiraki(): HikariDataSource {
    val config = HikariConfig()
    config.apply {
      driverClassName = "org.postgresql.Driver"
      jdbcUrl = System.getenv("JDBC_DB_URL")
      maximumPoolSize = 3
      transactionIsolation = "TRANSACTION_REPEATABLE_READ"
      config.validate()
    }

    return HikariDataSource(config)
  }

  suspend fun <T> dbQuery(block: () -> T): T = withContext(Dispatchers.IO) {
    transaction {
      block()
    }
  }
}

Something interesting to look at is the dbQuery function. It’s a suspend function to avoid making our queries blocking, meaning each query will start a new coroutine run on a dedicated thread pool.

With this, a reduced version of FlightsRepository used by our /flights endpoint looks like this:

object FlightsRepository {
  suspend fun add(flight: Flight, userId: String): Flight? {
    return dbQuery {
      val insertStatement = transaction {
        Flights.insert {
          it[bookingReference] = flight.bookingReference
          it[user] = userId
          it[departingDate] = DateTime(flight.departingDate)
          it[arrivalDate] = DateTime(flight.arrivalDate)
          it[origin] = flight.origin
          ...
        }
      }

      insertStatement.resultedValues?.get(0)?.toFlight()
    }
  }

  suspend fun getFlights(userId: String): List<Flight> {
    return dbQuery {
      Flights.select {
        (Flights.user eq userId)
      }.mapNotNull {
        it.toFlight()
      }
    }
  }

  suspend fun remove(id: String) {
    return dbQuery {
      Flights.deleteWhere {
        Flights.bookingReference eq id
      }
    }
  }

  private fun ResultRow.toFlight(): Flight {
    return Flight(
      bookingReference = this[Flights.bookingReference],
      departingDate = this[Flights.departingDate].toDate(),
      arrivalDate = this[Flights.arrivalDate].toDate(),
      origin = this[Flights.origin],
      ...
    )
  }
}

Authentication

Ktor has a very handy Authentication Feature using JWT. Quoting their documentation:

Ktor supports authentication out of the box as a standard feature. It supports mechanism to read credentials, and to authenticate principals.

It can be used in some cases along with the sessions feature to keep the login information between requests.

The topic is under development so the code to set up the feature is likely to change, meaning the best place to look at it is their documentation. How to use it shouldn’t change much though. This is how our LoginEndpoint.kt class looks like to implement our /login endpoint:

const val LOGIN_ENDPOINT = "/login"

@Location(LOGIN_ENDPOINT)
class Login

fun Route.loginApi(userRepository: UserRepository, jwtService: JwtService) {
  post<Login> {
    val params = call.receive<Parameters>()
    val userId = params["userId"] ?: return@post call.redirect(it)
    val password = params["password"] ?: return@post call.redirect(it)

    val user = userRepository.getUserWithHash(userId, hash(password))
    if (user != null) {
      val token = jwtService.generateToken(user)
      call.respondText(token)
    } else {
      call.respondText("Invalid user or password")
    }
  }
}

Clients will need to provide the generated bearer token to be able to use other endpoints. This is exactly what the authenticate bit was doing for the GET /flights endpoint that we saw earlier:

fun Route.flightsApi(flightsRepository: FlightsRepository) {

  authenticate("jwt") {
    get<FlightsApi> {
      val user = call.apiUser
      val flights = flightsRepository.getFlights(user.userId)
      call.respond(flights)
    }
    ...
  }
}

Web Client

Ktor allow us to build webs by using HTML/CSS and the Kotlin DSL or you can do what I did and use a JVM template engine called Freemarker.

First, we need to sort out the routing of our web. For this, we’re going to expand our Routing setup:

install(Routing) {

  static("/static") { resources("images") }

  //Web
  home(UserRepository)
  about(UserRepository)
  flights(UserRepository, FlightsRepository, userHashFunction)
  signIn(UserRepository, userHashFunction)
  signOut()
  signUp(UserRepository, userHashFunction)

  //Api
  flightsApi(FlightsRepository)
  loginApi(UserRepository, JwtService)
} 

Our Flights.kt, which fuels our web Flights page, will make sure the user is logged in, fetch its flights, and bind them to the free marker template.

const val FLIGHTS = "flights"

@Location(FLIGHTS)
class Flights

fun Route.flights(
  userRepository: UserRepository,
  flightsRepository: FlightsRepository,
  hashFunction: (String) -> String
) {
  get<Flights> {
    val user = call.sessions.get<UserSession>()?.let { userRepository.getUser(it.userId) }

    if (user == null) {
      call.redirect(SignIn())
    } else {
      val flights = flightsRepository.getFlights(user.userId)
      val date = System.currentTimeMillis()
      val code = call.securityCode(date, user, hashFunction)

      call.respond(
        FreeMarkerContent(
          template = "flights.ftl",
          model = mapOf(
            "flights" to flightsRepository.getFlights(user.userId).map(),
            "user" to user,
            "date" to date,
            "code" to code
          )
        )
      )
    }
  }

  post<Flights> {
    val user = call.sessions.get<UserSession>()?.let { userRepository.getUser(it.userId) }
    val params = call.receiveParameters()
    val date = params["date"]?.toLongOrNull() ?: return@post call.redirect(it)
    val code = params["code"] ?: return@post call.redirect(it)
    val action = params["action"] ?: throw IllegalArgumentException("Missing action")

    if (user == null || !call.verifyCode(date, user, code, hashFunction)) {
      call.redirect(SignIn())
    }

    when (action) {
      "delete" -> {
        val id = params["id"] ?: throw java.lang.IllegalArgumentException("Missing id")
        flightsRepository.remove(id)
      }
      "add" -> {
        flightsRepository.add(createFlight(params), user!!.userId)
      }
    }

    call.redirect(Flights())
  }
}

private fun createFlight(params: Parameters): Flight {
  val bookingReference = params["bookingReference"] ?: throw IllegalArgumentException("Missing argument: bookingReference")
  val origin = params["origin"] ?: throw IllegalArgumentException("Missing argument: destination")
  ...

  return Flight(
    bookingReference = bookingReference,
    departingDate = departingDate.getDate(),
    ...
  )
}

And finally, we’d have our FreeMarker file flights.ftl to draw in the browser (we’re using bootstrap for simplicity):

<#import "common/bootstrap.ftl" as b>

<@b.page>
<#if flights?? && (flights?size > 0)>

<table class="table table-striped">
    <thead>
    <tr>
        <th>Date</th>
        <th>Origin</th>
        <th>Destination</th>
        <th>Booking Reference</th>
        <th>Departing time</th>
        <th>Arrival time</th>
        <th>Airline</th>
        <th>People</th>
        <th>Paid price</th>

    </tr>
    </thead>
    <tbody>
    <#list flights as flight>
    <tr>
        <td style="vertical-align:middle"><h4>${flight.departingDate}</h4></td>
        <td style="vertical-align:middle"><h4>${flight.origin}</h4></td>
        <td style="vertical-align:middle"><h4>${flight.destination}</h4></td>
        <td style="vertical-align:middle"><h4>${flight.bookingReference}</h4></td>
        ...
        <td class="col-md-1" style="text-align:center;vertical-align:middle;">
            <form method="post" action="/flights">
                <input type="hidden" name="date" value="${date?c}">
                <input type="hidden" name="code" value="${code}">
                <input type="hidden" name="id" value="${flight.bookingReference}">
                <input type="hidden" name="action" value="delete">
                <input type="image" src="/static/delete.png" width="24" height="24" border="0 alt=" Delete" />
            </form>
        </td>
    </tr>
    </
    #list>
    </tbody>
</table>
</#if>

<h3>Insert new flight:</h3>
<div class="panel-body">
    <form method="post" action="/flights">
        <input type="hidden" name="date" value="${date?c}">
        <input type="hidden" name="code" value="${code}">
        <input type="hidden" name="action" value="add">
        Booking reference </br>
        <input type="text" name="bookingReference" required/>
        ...
        </br>
        <input type="submit" value="Submit"/>
    </form>
</div>
</@b.page>

Conclusion

That’s pretty much it! We now have a backend Ktor application, a web client and an api ready for mobile clients to interact with the backend, all built under the same project and language.

Hope you enjoyed the article. For any questions, my Linkedin and Twitter accounts are the best place. Thanks for your time!

You might find interesting…