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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Build an API with Kotlin on Google Cloud Platform
30 mins
- Getting Started
- Setting up Google Cloud
- Creating a Google Cloud Project
- Creating an AppEngine Application
- Installing Google Cloud
- Logging into Google Cloud
- Enabling Google Sheets API
- Authorizing Service Account
- Deploying to Google Cloud
- Adding Dynamic Data with Google Drive
- Reading Data From a Spreadsheet
- Combining Headers and Rows
- Building Agenda Entries
- Putting the Transformations Together
- Integrating the SpreadSheetDataSource
- Caching Response
- Mapping Entities
- Creating a Local Data Source
- Composing Data Sources
- Integrating the Cache
- Implementing Votes
- Updating LocalDataStore to save votes
- Creating the Voting Endpoint
- Storing Votes After Updates
- Where to Go From Here?
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")
}
}
}
- Retriev the
id
URL parameter and parse it to aLong
. - If there is no ID provided, you respond with a Bad Request error.
- If you have a valid ID, you place the vote for the user.
- If the vote didn't happen, you respond with a Not Found error.
- 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:
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
:
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 }
}
- First, you create a Map of IDs to votes from the old list of entries.
- 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.
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!