Build an API with Kotlin on Google Cloud Platform

In this tutorial you will learn how to build a server side API using Kotlin and Ktor that you can host on Google Cloud Platform and use with your Android app. By Pablo Gonzalez Alonso.

4.2 (5) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Implementing Votes

A read-only API is not the most interesting, as you could replace it with a static JSON file somewhere in the cloud and no one would know the difference.

Next, you are going to add the option to increment votes for a given talk, so you know which one is more popular.

Updating LocalDataStore to save votes

Go to LocalDataSource class and add the following methods:

fun vote(id: Long) =
  find(byId = id)?.also { it.incrementVotes() }

private fun find(byId: Long): AgendaEntry? = try {
  dataStore.get(KeyFactory.createKey(AGENDA_ENTRY_NAME, byId)).toEntry()
} catch (e: EntityNotFoundException) {
  null
}

private fun AgendaEntry.incrementVotes() {
  copy(votes = votes + 1).toEntity().also { dataStore.put(it) }
}

The private method find(byId: Long) looks for an entity in dataStore and converts it to an AgendaEntry. It returns null if the DataStore throws an exception because it couldn't find the expected value.

In the other private method, incrementVotes() you make a copy using the copy constructor, incrementing the votes.

And, finally, vote() combines these two, returning the mutated AgendaEntry if successful or null otherwise.

Creating the Voting Endpoint

The main function doesn't have direct access to the LocalDataSource class, so you’ll have to add a new method to the AgendaEntryRepository class to be able to vote for an entry.

fun vote(id: Long) = cacheDataSource.vote(id)

Now you have everything you need to create the relevant vote endpoint. Go to the main function and add a new route in the routing block:

post("vote") {
// 1
  when (val id = call.request.queryParameters["id"]?.toLong()) {       
// 2             
    null -> call.respond(HttpStatusCode.BadRequest, "Missing id parameter")  
// 3             
    else -> when (entries.vote(id)) {                                
// 4               
      null -> call.respond(HttpStatusCode.NotFound, "No entry found for id: $id") 
// 5
      else -> call.respondText("Added vote for id: $id")                            
    }
  }
}
  1. Retriev the id URL parameter and parse it to a Long.
  2. If there is no ID provided, you respond with a Bad Request error.
  3. If you have a valid ID, you place the vote for the user.
  4. If the vote didn't happen, you respond with a Not Found error.
  5. Otherwise, return a success message.

This time, to see this last change in action you’ll have to deploy to the cloud again. Open up the Android app and you’ll be able to see how you can now vote for talks:

Vote for talks in the Android app

Storing Votes After Updates

When the server updates the cache from the spreadsheet, the vote counts will be lost. The loss occurs because the entries that you get from the spreadsheet have no information about the votes.

One way to replicate this behavior is to temporarily deploy a version of the server with the cachePeriod inside AgendaEntryRepository set to 0:

Persist votes for talks

The vote count remains at zero, as the server overwrites it when updating.

One way to fix this is to make sure you are keeping the votes when updating the cache from the spreadsheet. Add the following function to the AgendaEntryRepository.kt file to do this:

private fun List<AgendaEntry>.updateVotes(old: List<AgendaEntry>): List<AgendaEntry> {
  // 1
  val votes = old.map { it.id to it.votes }.toMap()
  // 2
  return map { entry -> votes[entry.id]?.let { entry.copy(votes = it) } ?: entry }
}
  1. First, you create a Map of IDs to votes from the old list of entries.
  2. Then, if there are votes for a given entry, you create a copy with the votes; otherwise you can just return the current entry.

Add a call to this function by updating the listAll() function in the AgendaEntryRepository class, just before the spot where you save the entries into the dataStore, so the server will persist the votes:

fun listAll(): List<AgendaEntry> = cacheDataSource.listAll().let { cached ->
  cached.takeUnless { it.requiresUpdate(cachePeriod) }
      ?: spreadSheetDataSource.listAll()
          .updateVotes(cached) // <-- added call to new function
          .also { entries -> cacheDataSource.save(entries) }
}

Deploy the server again, keeping cachePeriod as 0. The mobile app now increments the votes.

Persist Votes in Android App

Now that you have verified that it works, you can restore the one hour cachePeriod. The server code is ready for consumers.

Where to Go From Here?

You can download the final project at the top or bottom of this tutorial.

Checkout the official documentation to explore more about Ktor.

You can also find lots of examples to learn from at Ktor’s collection of samples. Our video course Server-Side Kotlin with Ktor covers using the Ktor framework to build a Kotlin web app and API, and shows how to deploy the app to Heroku and also run the app in a Docker container.

As a challenge, let's say that your event is super-successful, so now your company has the money to have a multi-day conference. You need to add multi-day support in the app.

How would you modify both the server-side and the mobile app to support such changes? Leave your solutions in the comments below!

I hope you enjoyed the tutorial, and please leave any questions or comments below!

Pablo Gonzalez Alonso

Contributors

Pablo Gonzalez Alonso

Author

Jason Donmoyer

Tech Editor

Chris Belanger

Editor

Julia Zinchenko

Illustrator

Nishant Srivastava

Final Pass Editor

Eric Soto

Team Lead

Over 300 content creators. Join our team.