Ktor: Crafting a Stock Portfolio Endpoint – Part 2

Ktor: Crafting a Stock Portfolio Endpoint – Part 2

This is the second post in the series of posts that aim to build a single endpoint with Ktor by wrapping the AlphaVantage Quote Endpoint. If you haven't read Part 1, please at least skim through it to make sure we have a similar Ktor setup before continuing. If you have finished setting up all the code in Part 1, let's dive right in.

Secret API Key

One of the required fields of the Alpha Vantage endpoint is apikey. The API key is used to determine the usage limit based on the plan. Most Alpha Vantage APIs can be accessed for free with the standard limit of 25 requests per day. Even with the most expensive premium plan of $249.99/month, there is still a limit of 1200 requests per minute. For that reason, the API key should be kept private and not committed to the GitHub repo. Otherwise, malicious users could exhaust your limit, rendering your server unusable. To learn more about API keys, you can read this Google Guide.

Storing the API key in an environment variable is arguably the simplest and most common method to hide it from the public. On the topic of environment variables, CircleCI has a great post explaining the basics. Then, assign your API key to an environment variable, as demonstrated in this guide from Nanyang Technology University (NTU).

export ALPHA_VANTAGE_API_KEY=AU1TUMN2IS3BEAU4TI5FUL

.zshrc

Congratulations! You have set your API key to an environment variable. The next step is to access ALPHA_VANTAGE_API_KEY from the application code. Remember choosing YAML for configuration at the beginning? You can now utilize the application.yaml to access the variable.

ktor:
    //...
    environment: $ALPHA_VANTAGE_API_KEY

application.yaml

To test whether the key can be accessed correctly, you can modify the Routing.kt to return the API key to the browser.

//...
fun Application.configureRouting() {
    val apiKey = environment.config.propertyOrNull(KTOR_ENV)?.getString()
    routing {
        get("/") {
            apiKey?.let {
                call.respond(Response(it))
            } ?: call.respond(Response("Failed to get API key!"))
        }
    }
}

// Create a constant to access the environment variable set up in the application.yaml
private const val KTOR_ENV = "ktor.environment"

Routing.kt

Running the application again should result in the browser showing the response with the API key as the value.

Response with the API Key

Response Model

Most public endpoints return a response in JSON format because it can be used in Javascript (the main language for most web applications) without the need for parsing or serializing. Unfortunately, a Ktor application requires parsing a JSON response to Kotlin code to extract the data effectively. First, let's take a look at a sample response from the Alpha Vantage endpoint.

{
    "Global Quote": {
        "01. symbol": "IBM",
        "02. open": "161.0000",
        "03. high": "161.7300",
        "04. low": "160.0800",
        "05. price": "160.1000",
        "06. volume": "4086065",
        "07. latest trading day": "2024-01-03",
        "08. previous close": "161.5000",
        "09. change": "-1.4000",
        "10. change percent": "-0.8669%"
    }
}

Sample Response from Alpha Vantage Quote Endpoint

A simple data class that mimics the structure of the above sample response should work just fine.

@Serializable
data class AlphaVantageQuote(
    val globalQuote: GlobalQuote,
)

@Serializable
data class GlobalQuote(
    val symbol: String,
    val open: Double,
    val high: Double,
    val low: Double,
    val price: Double,
    val volume: Long,
    val latestTradingDay: String,
    val previousClose: Double,
    val change: Double,
    val changePercent: Double,
)

AlphaVantageQuote.kt

Don't forget to add the @Serializable annotations on top of the data classes so it can be parseable from the JSON. Those stem from the plugin that you will install in the next section.

API Call

Calling the Alpha Vantage endpoint means the server becomes a client, which requires a different set of plugins. By now, it's probably become second nature when it comes to adding new dependencies.

dependencies {
  //...
  implementation("io.ktor:ktor-client-core:$ktor_version")
  implementation("io.ktor:ktor-client-okhttp:$ktor_version")
  implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
}

build.gradle.kts

With all the dependencies set up, you can jump straight to creating a function that calls the API by following the Ktor doc. However, I'm going to apply the Bridge design pattern mainly for testability and cross-platform compatibility.

interface AlphaVantageApi {
    suspend fun getQuote(
        symbol: String,
        apiKey: String,
    ): AlphaVantageQuote
}

AlphaVantageApi.kt

class AlphaVantageApiImpl : AlphaVantageApi {
    override suspend fun getQuote(
        symbol: String,
        apiKey: String,
    ): AlphaVantageQuote {
        // Installing the plugin for client network call
        val httpClient = HttpClient {
            install(ContentNegotiation) {
                json(
                    Json {
                        prettyPrint = true
                        isLenient = true
                        ignoreUnknownKeys = true
                    }
                )
            }
        }
        
        // Perform the network call with parameters
        val response = httpClient.get {
            url {
                protocol = URLProtocol.HTTPS
                host = ALPHA_VANTAGE_URL
                path("query")
                parameters.append(name = "function", value = "GLOBAL_QUOTE")
                parameters.append(name = "symbol", value = symbol)
                parameters.append(name = "apikey", value = apiKey)
            }
        }
        // Close the client connection after finish calling the API
        httpClient.close()
        
        // Return only the body
        return response.body<AlphaVantageQuote>()
    }
}

private const val ALPHA_VANTAGE_URL = "www.alphavantage.co"

AlphaVantageApiImpl.kt

Note that the HttpClient.get is a suspending function, so getQuote must be a suspending function as well to handle the network call asynchronously. To learn more about suspending functions and Kotlin coroutine, check out this awesome introduction video.

Next, you can simply call the API in the Routing.kt to test by printing the response to the browser. By now, the main function configureRouting should look something like this.

fun Application.configureRouting() {
    val apiKey = environment.config.propertyOrNull(KTOR_ENV)?.getString()
    val api = AlphaVantageApiImpl()
    routing {
        get("/") {
            apiKey?.let {
                val quote = api.getQuote(
                    symbol = "IBM",
                    apiKey = apiKey,
                )
                call.respond(quote)
            } ?: call.respond(Response("Failed to get API key!"))
        }
    }
}

Routing.kt

Serial Name

After running the current code, the browser displays an error. Fear not. Dealing with errors is always part of the process.

Error from the Browser

As you may have guessed, @Serializable annotation isn't smart enough to parse such a weirdly formatted field name from the Alpha Vantage endpoint. In other words, it doesn't know that "Global Quote" should be parsed as globalQuote or "07. latest trading day" to latestTradingDay. To fix this, you can use the annotation @SerialName as suggested in the error message.

@Serializable
data class AlphaVantageQuote(
    @SerialName("Global Quote")
    val globalQuote: GlobalQuote,
)

@Serializable
data class GlobalQuote(
    @SerialName("01. symbol")
    val symbol: String,
    @SerialName("02. open")
    val open: Double,
    @SerialName("03. high")
    val high: Double,
    @SerialName("04. low")
    val low: Double,
    @SerialName("05. price")
    val price: Double,
    @SerialName("06. volume")
    val volume: Long,
    @SerialName("07. latest trading day")
    val latestTradingDay: String,
    @SerialName("08. previous close")
    val previousClose: Double,
    @SerialName("09. change")
    val change: Double,
    @SerialName("10. change percent")
    val changePercent: String,
)

AlphaVantageQuote.kt

Now, the plugin should know how to map each field from the JSON response to the corresponding Kotlin model. The server application is now completed.

Conclusion

That's it! Congratulations, you have successfully wrapped the Alpha Vantage Quote Endpoint in a Ktor server that returns a Kotlin response. However, it is not complete without proper unit tests, which will be explored in Part 3. Stay tuned for Part 3 by subscribing to our newsletter, and please share your thoughts or questions in the comments section below!

GitHub Repo

GitHub - trinhan-nguyen/ktor-ferreter
Contribute to trinhan-nguyen/ktor-ferreter development by creating an account on GitHub.

Reference

Why and when to use API keys | Cloud Endpoints with OpenAPI | Google Cloud
Introduction to environment variables - CircleCI
Introduction to environment variables in CircleCI
Environment Variables for Java Applications - PATH, CLASSPATH, JAVA_HOME
Ktor and Kotlin: Crafting a Stock Portfolio Endpoint – Part 1
Let’s build a simple server for a stock market-related app using Ktor - a lightweight Kotlin framework. Part 1 only shows how to set up a Ktor project and return a type-safe response.