Serverless Kotlin on Google Cloud Run

Learn how to build a serverless API using Ktor then dockerize and deploy it to Google Cloud Run. By Kshitij Chauhan.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Starting the Server

In the Application.kt file, add a main method that starts the server:

val server = ...

fun main() {
  server.start(wait = true)
}

The wait parameter tells the application to block until the server terminates.

At this point, you have everything you need to get a basic server up and running. To start the server, use the green icon next to main IntelliJ:


Screenshot with an arrow that points to the "run" icon in IntelliJ

If everything went well, you’ll see logs indicating your server is running!


Screenshot that shows logs produced by a running Ktor server

To test your server, use the curl command line utility. Enter the following command in the terminal:

curl -X GET "http://0.0.0.0:8080/"

You’ll see the correct response: “Hello, world!”.

➜  ~ curl -X GET "http://0.0.0.0:8080/"
Hello, world!
Note: If you get a response similar to curl: (7) Failed to connect to 0.0.0.0 port 8080 after 0 ms: Address not available, replace your curl request with curl -X GET "http://0.0.0.0:8080/".
You could update the networking configuration in order to continue using 0.0.0.0 but that’s outside the scope of this tutorial. In subsequent requests, you’ll have to keep using localhost instead of 0.0.0.0.

Detecting the Client’s IP Address

In the routing function, add a route that returns the client’s IP address back to them. To get the client’s IP address, use the origin property of the request object associated with a call.

import io.ktor.server.plugins.*

// Add this in the `routing` block:
get("/ip") {
  val ip = call.request.origin.remoteHost
  call.respond(ip)
}

This adds an HTTP GET route on the “/ip” path. On each request, the handler extracts the client’s IP address using call.request.origin.remoteHost and returns it in the response.

Restart the server, and try this new route using curl again:

➜  ~ curl -X GET "http://0.0.0.0:8080/ip"
localhost%
➜  ~

The server responds with localhost, which just means the client and server are on the same machine.

Fetching Locations Using IP Addresses

To fetch a client’s location from their IP address, you need a geolocation database. There are many free third-party services that let you query geolocation databases. IP-API is an example.

IP-API provides a JSON API to query the geolocation data for an IP address. To interact with it from your server, you’ll need to make HTTP requests to it using an HTTP client. For this tutorial, you’ll use the Ktor client.

Additionally, you’ll need the ability to parse JSON responses from IP-API. Parsing and marshalling JSON data is a part of data serialization. Kotlin has an excellent first-party library, kotlinx.serialization, to help with it.

Note: If you’re unfamiliar with the Kotlinx Serialization library, consider reading Android Data Serialization Tutorial with the Kotlin Serialization Library.

The process of detecting the client’s location will look like this:


Detecting client's location

Adding Kotlinx Serialization and Ktor Client

The kotlinx.serialization library requires a compiler plugin as well as a support library.

Add the compiler plugin inside the plugins of the build.gradle.kts file:

plugins {
  // ...
  kotlin("plugin.serialization") version "1.6.10"
}

Then add these dependencies to interop with it using Ktor:

dependencies {
  // ...
  implementation("io.ktor:ktor-client-core:$ktorVersion")
  implementation("io.ktor:ktor-client-cio:$ktorVersion")
  implementation("io.ktor:ktor-client-serialization:$ktorVersion")
  implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
  implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
}

Here’s a description of these artifacts:

  • io.ktor:ktor-client-core provides core Ktor client APIs.
  • io.ktor:ktor-client-cio provides a Coroutines-based Ktor client engine.
  • io.ktor:ktor-client-serialization, io.ktor:ktor-serialization-kotlinx-json and io.ktor:ktor-client-content-negotiation provide APIs to serialize request/response data in JSON format using the kotlinx.serialization library.

Using Ktor Client

So far you’ve used Ktor as an application server. Now you’ll use the other side of Ktor: an HTTP client.

First, create a data class to model the responses of IP-API. Create a file named IpToLocation.kt, and add the following code to it:

package com.yourcompany.android.serverlesskt

import kotlinx.serialization.Serializable

@Serializable
data class LocationResponse(
  val country: String,
  val regionName: String,
  val city: String,
  val query: String
)

Then, create a function that sends an HTTP request to IP-API with the client’s IP address. In the same file, add the following code:

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*

/**
 * Specifies which fields to expect in the
 * response from the API
 *
 * More info: https://ip-api.com/docs/api:json
 */
private const val FIELDS = "country,regionName,city,query"

/**
 * Prefix URL for all requests made to the IP to location API
 */
private const val BASE_URL = "http://ip-api.com/json"

/**
 * Fetches the [LocationResponse] for the given IP address
 * from the IP to Location API
 *
 * @param ip The IP address to fetch the location for
 * @param client The HTTP client to make the request from
 */
suspend fun getLocation(ip: String, client: HttpClient): LocationResponse {
  // 1
  val url = buildString {
    append(BASE_URL)
    if (ip != "localhost" && ip != "_gateway") {
      append("/$ip")
    }
  }

  // 2
  val response = client.get(url) {
    parameter("fields", FIELDS)
  }

  // 3
  return response.body()
}

getLocation fetches the location data for an IP address using IP-API. It uses an HttpClient supplied to it to make the HTTP request.

First, it constructs the URL to send the request to. Second, it adds FIELDS as a query parameter to the URL. This parameter tells IP-API which fields you want in the response (learn more here). Finally, it sends an HTTP GET request to the constructed URL and returns the response.

Fetching Location Data

To use getLocation, you must create an instance of the Ktor HTTP client. In the Application.kt file, add the following code above main:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
import io.ktor.serialization.kotlinx.json.*

val client = HttpClient(CIO) {
  install(ClientContentNegotiation) {
    json()
  }
} 

This not only creates an HttpClient, but it also adds a Ktor feature ContentNegotiation (aliased as ClientContentNegotiation to avoid import collision with the server feature of the same name) for JSON serialization/deserialization.

Then, add a route to your server to fetch the location data. In the routing block, add the following route:

get("/location") {
  val ip = call.request.origin.remoteHost
  val location = getLocation(ip, client)
  call.respond(location)
}

Note this route responds with an object of type LocationResponse, which should be deserialized to the JSON format before sending it to the client. To tell Ktor how to deal with this, install the server-side ContentNegotiation plugin.

First, add the following dependency in the build.gradle.kts file:

implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")

In the Application.kt file, modify the configuration block for embeddedServer by adding the following code:

import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation

val server = embeddedServer(Netty, port=8080) {
  install(ServerContentNegotiation) {
    json()
  }
  // ...
}

Finally, restart the server and use curl to send a request to the “/location” route. You’ll see a response like this:

➜  ~ curl -X GET "http://0.0.0.0:8080/location"
{
  "country":"<country>",
  "regionName":"<state>",
  "city":"<city>,
  "query":"<ip>"
}
➜  ~

That’s it for your back-end API! So far you’ve built three API routes:

  • /: Returns “Hello, world!”.
  • /ip: Returns the client’s IP address.
  • /location: Fetches the client’s IP geolocation data and returns it.

The next step is to containerize the application to deploy it on Cloud Run.