How To Create a Ktor Client To Connect To OpenWeatherMap API

Featured image for the article about Ktor client and OpenWeatherMap contains Ktor logo in the foreground and a blurred photo of a forest in winter in the background.

This comprehensive guide is about setting up a Ktor client that effortlessly retrieves data from OpenWeatherMap API. I’ll tackle client configuration, API calls, models configuration, and error handling, leaving you with a weather-fetching client ready to be used on any platform.

Ktor is a flexible library that provides the ability to create a non-blocking HTTP client, which enables you to perform requests and handle responses. Its functionality could be enhanced with plugins, such as logging, serialization, authentication, and so on. Its core benefit is that it’s a lightweight framework that can be used on different platforms such as Android, JVM, or Native!

General Overview

Let’s discuss some basic information for everyone to be on the same page.

Today, we are going to write HTTP requests and receive responses. There are different types of requests but OpenWeatherMap for the most part utilizes GET requests with various parameters.

OpenWeatherMap API, as you can tell by its name, is an API for retrieving weather-related data. It provides various types of data but we’re going to use only 3 of them:

  • Current Weather,
  • 3-hours Forecast,
  • and Air Pollution API.

To start using OpenWeatherMap you need to register and get your special API key. It has lots of free options and for our sake, we don’t need to pay at all.

But be aware of the limitations of the call: 1,000 API calls per day for free!

Gradle Dependencies

For this project, we’re going to use basic Ktor’s client, serialization, engines, and logging.

Your build.gradle.kts file would look like this:

val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

plugins {
    kotlin("jvm") version "1.9.21"
    id("io.ktor.plugin") version "2.3.6"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.21"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.ktor:ktor-client-core-jvm")
    implementation("io.ktor:ktor-client-cio-jvm")

    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-client-content-negotiation-jvm")

    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-client-logging:$ktor_version")
    implementation("io.ktor:ktor-client-apache5:$ktor_version")

    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

Creating Models

The first step of creating a Ktor client is actually revealing what we’re gonna handle. Basically, we need 2 types of models: request and response. All necessary information is located on the corresponding page of the documentation. The documentation is pretty neat and, generally, it’s a good practice to check it out during the coding!

To optimize the process we could spot similar parameters appearing in the Current Weather and Weather Forecast. Such as Mode and Units.

So we can create separate classes and use the latter like this:

enum class ResponseUnits {
    STANDARD,
    METRIC,
    IMPERIAL
}

enum class ResponseMode {
    XML
}

Now let’s create the request themselves.

As there are some optional parameters present we’re going to make them nullable with default value null so that we could use them or not with ease.

Here are the models for requests:

data class CurrentWeatherRequest(
    val latitude: Double,
    val longitude: Double,
    val mode: ResponseMode? = null,
    val units: ResponseUnits? = null,
    val language: String? = null
)

data class WeatherForecastRequest(
    val latitude: Double,
    val longitude: Double,
    val mode: ResponseMode? = null,
    val units: ResponseUnits? = null,
    val language: String? = null,
    val count: Int? = null
)

data class AirPollutionRequest(
    val latitude: Double,
    val longitude: Double
)

Consequently, we desperately need models to put our data to: the response models.

It’s the most routine process because there are some huge responses and by ‘huge’ I mean lots of parameters. Let’s dive deep into one model and the rest would be easy to create.

Here’s what our JSON of current weather data looks like (there is also a text representation of this JSON with more information about parameters):

{
  "coord": {
    "lon": 10.99,
    "lat": 44.34
  },
  "weather": [
    {
      "id": 501,
      "main": "Rain",
      "description": "moderate rain",
      "icon": "10d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 298.48,
    "feels_like": 298.74,
    "temp_min": 297.56,
    "temp_max": 300.05,
    "pressure": 1015,
    "humidity": 64,
    "sea_level": 1015,
    "grnd_level": 933
  },
  "visibility": 10000,
  "wind": {
    "speed": 0.62,
    "deg": 349,
    "gust": 1.18
  },
  "rain": {
    "1h": 3.16
  },
  "clouds": {
    "all": 100
  },
  "dt": 1661870592,
  "sys": {
    "type": 2,
    "id": 2075663,
    "country": "IT",
    "sunrise": 1661834187,
    "sunset": 1661882248
  },
  "timezone": 7200,
  "id": 3163858,
  "name": "Zocca",
  "cod": 200
}

Now we have to face some difficulties. Not only do we have to parse it but also there are some optional parameters.

To solve the first issue we mark our classes with @Serializable and @SerialName annotations. I highly recommend using @SerialName because it provides enhanced readability and usability especially when handling such long classes.

To solve the second problem I recommend you to read my detailed guide that covers all essential information about Serialization. Basically, if we want to parse all of the data, we make optional fields with @EncodeDefault and make it nullable with the default value as null. This makes sure that if there was no such a field we would get a null, without any exceptions.

Now let’s look at classes for our Ktor client:

@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class CurrentWeather(
    @SerialName("coord") val coordinates: Coordinates,
    @SerialName("weather") val weather: List<Weather>,
    @SerialName("base") val base: String,
    @SerialName("main") val main: Main,
    @SerialName("visibility") val visibility: Int,
    @SerialName("wind") val wind: Wind,
    @SerialName("rain")
    @EncodeDefault
    val rain: Rain? = null,
    @SerialName("snow")
    @EncodeDefault
    val snow: Snow? = null,
    @SerialName("clouds") val clouds: Clouds,
    @SerialName("dt") val dateTime: Long,
    @SerialName("sys") val systemData: SystemData,
    @SerialName("timezone") val timezone: Int,
    @SerialName("id") val cityId: Int,
    @SerialName("name") val cityName: String,
    @SerialName("cod") val code: Int
) {

    //other classes here

    @Serializable
    data class Wind(
        @SerialName("speed") val speed: Double,
        @SerialName("deg") val direction: Int,
        @SerialName("gust") val gust: Double
    )

    @Serializable
    data class Rain(
        @SerialName("1h")
        @EncodeDefault
        val oneHour: Double? = null,
        @SerialName("3h")
        @EncodeDefault
        val threeHours: Double? = null
    )

	//other classes here
}

The rest of the responses are quite similar so you could check them out in the repository that comes with this article.

Quick remark, I’ve made classes such as Coordinates, Weather, etc. as inner classes since there are the same classes for other responses but with different structures.

This approach helps not to mix them up.

Creating The Ktor Client Itself

We are gonna be using Ktor’s HttpClient with some adjustments and extended configuration.

Let’s check it out:

private val client = HttpClient(Apache5) {
        install(ContentNegotiation) {
            json(Json {
                isLenient = false
            })
        }
        install(Logging)
        defaultRequest {
            url {
                protocol = URLProtocol.HTTPS
                host = "api.openweathermap.org/data/2.5"
            }
        }
    }

First of all, we have the Apache5 engine rather than CIO, for example, because it’s suitable for HTTP/2 while CIO is not.

Next, we have 2 plugins: the ContentNegotiation for the JSON parsing and the Logging for detailed logs.

Also, as all of our requests to OpenWeatherMap have similar endpoints, we can use defaultRequest to shorten our request’s links. The whole defaultRequest translates to https://api.openweathermap.org/data/2.5/. We’ll combine this with the request’s specific parameters.

Now let’s talk about them.

Creating Routes

As all of our routes would share similar logic let’s look closely at only one of them for the Current Weather:

suspend fun getCurrentWeather(currentWeatherRequest: CurrentWeatherRequest): CurrentWeather? {
        return try {
            val response: HttpResponse = client
                .get("/weather") {
                    url {
                        parameters.append("lat", currentWeatherRequest.latitude.toString())
                        parameters.append("lon", currentWeatherRequest.longitude.toString())
                        parameters.append("appid", APP_ID)
                        if (currentWeatherRequest.mode != null) parameters.append(
                            "mode",
                            currentWeatherRequest.mode.name()
                        )
                        if (currentWeatherRequest.units != null) parameters.append(
                            "units",
                            currentWeatherRequest.units.name()
                        )
                        if (currentWeatherRequest.language != null) parameters.append(
                            "lang",
                            currentWeatherRequest.language
                        )
                    }
                }

            if (response.status == HttpStatusCode.OK) {
                response.body<CurrentWeather>()
            } else {
                println("Failed to retrieve current weather. Status: ${response.status}")
                null
            }
        } catch (e: Exception) {
            println("Error retrieving current weather: ${e.message}")
            null
        }
    }

Here we create GET HttpResponse with the path “/weather” and then we specify additional parameters, note that it’ll automatically use the correct notation(? and other).

In the URL block, we list all of our parameters using parameters.append() function that takes the name and the value. There is no need to include optional parameters that are not specified, so we simply check whether they’re null or not.

Our Ktor client also implements error handling.

For the errors with the request itself, such as connection problems, any issues from the server, etc. we’ll get the “Error retrieving current weather:” message and null result.

And if your request is successful but has some undesired response, in our case everything except 200 OK, we’ll get “Failed to retrieve current weather. Status:” and null result as well.

Ktor Client In Action

To use the client you could simply call our object with the corresponding method like this:

WeatherClient.getCurrentWeather(
        CurrentWeatherRequest(
            latitude = 44.34,
            longitude = 10.99,
            units = ResponseUnits.IMPERIAL,
            language = "pl"
        )
    )
	
	WeatherClient.getWeatherForecast(
        WeatherForecastRequest(
            latitude = 44.34,
            longitude = 10.99,
            units = ResponseUnits.IMPERIAL,
            language = "pl"
        )
    )
	
	WeatherClient.getAirPollution(
        AirPollutionRequest(
            latitude = 50.0,
            longitude = 50.0
        )
    )

This could be used on every platform but as I’ve mentioned before make sure you’re using the correct engine!

Summary

And that’s all for this article about the OpenWeatherMap API Ktor client.

If you would like to download the full source code, then you can find it in this GitHub repository.

Lastly, thank you for being here, and happy to hear your feedback in the comments section below!

Share this:

Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Related content

Newsletter

Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.