Full Stack Weather Forecast App

weather station

Background

What started as a foray into asynchronous programming with Kotlin has evolved into a full blown web-app! I started working on a simple app last year that gathered data from the NWS Weather API just to showcase asynchronous programming in Kotlin via coroutines, but I put the development on pause to wrap up the Web Developer Bootcamp course on Udemy (an amazing course, I highly recommend!). After completing the web developer course I wanted to learn a frontend JavaScript framework to round out my web dev skills, so I created a simple weather forecast app in React that is basically a frontend for the NWS Weather API. All was well and good, but I noticed that the API hosted on weather.gov was fairly unstable with calls to the API resulting in 503 responses or just plain ol’ timing out.

I picked this Kotlin based weather app back up with the plan to rewrite the frontend in React (which was originally written in Kotlin/JS), while keeping the backend as a Ktor application. I also switched over to using the OpenWeather API in place of the NWS Weather API for stability. Below is an attempt at a UML sequence diagram outlining the flow of calls to the various APIs once the user of the app submits a location.

uml sequence diagram

Why Ktor?

The Spring framework is popular when it comes to developing RESTful services in Java or Kotlin, but as many Java developers know Spring Boot apps require a good amount of boilerplate to get things off the ground, and the Java programming language can be quite verbose on its own. With me coming back to Java from the world of JavaScript I wanted to spin up a Java backend with as little code as possible, while still keeping the performance that a multithreaded programming language like Java offers. From working on a few Kotlin + Vaadin + Spring Boot apps at my previous employer I’ve found that Kotlin is more similar to JavaScript syntactically compared to Java, thus I knew that I wanted to write the backend for this project in Kotlin. I started to refresh myself on spinning up a Kotlin + Spring Boot app, but I happened to stumble on Ktor during my research and decided to give it a try. Ktor is built by the awesome folks at JetBrains and is marketed as a lightweight framework with first class support for asynchronous programming (via Kotlin’s own coroutines library). At the end of the day Ktor seemed like a great choice for getting an MVP going in my case.

Dependency Injection: Insert Koin to Play

One thing that Ktor doesn’t provide out of the box is dependency injection. At first this wasn’t a problem as my backend was fairly simple - it only reached out to a single external API. However, I decided to add functionality to the frontend that required an extra call to a different API (specifically the Geocoding API from Mapbox). This new functionality allowed me to gather the geographic coordinates for a given city + state; these coordinates were then used to fetch the forecast data from OpenWeather. I suddenly found that I had a service layer object (that gathered forecast data from OpenWeather) holding the responsibility to build another complex object on the same layer (which gathered data from Mapbox). This service-layer <-> service-layer dependency resulted in some messy test code. Luckily I stumbled onto a wonderful guide written by Marco Gomiero on structuring a Ktor project. Marco’s guide contains info on integrating the Koin library into a Ktor app - Koin adds dependency injection capabilities to Ktor.

Routing in Ktor

Defining routes for an API in a Ktor app can be done in a modular way. For this weather forecast app I have all of the routes defined in a separate ForecastResource.kt file. This ForecastResource file contains the route definition and the code that is executed when a request hits the route, much like a RestController class in a Spring app. See the following code for the ForecastResource endpoint as a quick example for defining a route and it’s implementation code:

@Location("/forecast")
class ForecastEndpoint {

    // route definition
    @Location("")
    data class Forecast(val parent: ForecastEndpoint, val city: String, val state: String)
}

fun Route.forecastEndpoint() {

    // route implementation
    get<ForecastEndpoint.Forecast> {
        // code to retrieve forecast data goes here
    }
}

However, defining a route and its implementation won’t automatically register the route in Ktor. To register routes in a Ktor app we must add the routes explicitly in the Application.configureRouting() method - the following code adds our route from ForecastResource into the Ktor application’s routing registry:

import dev.calebmiller.application.features.forecast.resource.forecastEndpoint

fun Application.configureRouting() {
    install(Locations)

    routing {
        // add all of the routes defined in ForecastResource
        forecastEndpoint()
    }
}

Adding routes explicitly via code in Ktor is similar to adding routes via the Router class in an Express.js app, compared to relying on the good ol’ Spring annotation magic (e.g. @GetMapping, @PostMapping) to register your routes behind the scenes. For example, in an Express app I can have my forecast route defined in a forecasts.js file:

const express = require("express");
const router = express.Router();

// route definition
router.route("/")
    .get(
        // route implementation - forecastController has the code to retrieve 
        // the forecast data
        forecastController.getForecast
    );

module.exports = router;

Then I can add the route(s) from forecasts.js into the Express app route registry with the following code:

const express = require("express");
const forecastRoutes = require("./routes/forecasts");

const app = express();

app.use("/forecast", forecastRoutes);

Ktor not making heavy use of annotation based configuration and instead opting for code based configuration makes the jump between JavaScript and Kotlin development much less painful in my experience, this is in contrast to all of the different annotations one must use for configuration in a Spring app.

Requests to External Services with Ktor Client

Ktor also includes a client library that can be used to make HTTP requests to external APIs, this client can also be used in a Kotlin Multiplatform project to tie directly into your backend. Using the Ktor Client to make requests to the Mapbox and OpenWeather APIs was pretty straightforward - the code used to make these calls has a similar look and feel to using the Axios library to make HTTP requests in JavaScript. Here’s an example of using the Ktor client to make a call to the OpenWeather API to get forecast data for a given location:

private val client = HttpClient(engine) {
    // any client configuration goes here
}

val response: HttpResponse = client.request {
    method = HttpMethod.Get
    url {
        protocol = URLProtocol.HTTPS
        host = "api.openweathermap.org"
        path("data", "2.5", "onecall")
        parameter("lat", latitude)
        parameter("lon", longitude)
        parameter("exclude", "minutely,alerts")
        parameter("units", "imperial")
        parameter("appid", openWeatherAccessToken)
    }
}

if (response.status == HttpStatusCode.OK) {
    // handle response data
    val responseBody: String = response.receive()
} else {
    // handle error response from server
}

And here’s how I could make that same call in JavaScript using Axios for comparison:

import axios from "axios";

axios({
    method: 'get',
    baseURL: 'https://api.openweathermap.org/',
    url: '/data/2.5/onecall',
    params: {
        lat: latitude,
        lon: longitude,
        exclude: "minutely,alerts",
        units: "imperial",
        appid: openWeatherAccessToken
    }
})
.then(response => {
    // handle response data
    const responseBody = response.data;
})
.catch(error => {
    // handle error response from server
});

Kotlin code can be very similar to JavaScript indeed. 🧐

The Ktor Client includes a JSON plugin that can parse JSON data from the response body into an instance of a given data class - this made working with the large datasets returned from external APIs easy to work with in my case. Ktor Client also includes a MockEngine class that is very useful when writing tests for code that makes use of a client. Using a MockEngine you can easily mock out calls to external APIs to cover different scenarios (e.g. success or error responses). For example, the following code uses a MockEngine to call the OpenWeather API in a unit test that covers receiving a success response when calling the API:

class OpenWeatherServiceTest {

    @Test
    fun getOneCallForecast_whenValidRequest_thenForecastDataReturned() = runTest {
        val responseBody = Files.readAllBytes(
            Path(
                "src/test/resources/openWeatherApiResponses/oneCallResponse.json"
            )
        )
        val mockEngine = MockEngine { request ->
            respond(
                content = ByteReadChannel(responseBody),
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, "application/json")
            )
        }

        val openWeatherApiClient = OpenWeatherService(mockEngine)
        val response = openWeatherApiClient.getWeatherForecast(90.0, 90.0)

        Assert.assertEquals(43.3, response.current.temperature)
    }
}

Final Thoughts

All in all using Ktor to build this simple backend was a great experience, and I’ve ended up with an app I can use as a starting point for future Ktor projects. With all that I’ve covered in this post I’ve just scratched the surface of Ktor and all of the functionality it has. If this post has piqued your interest in Ktor I encourage you to head over to the official site and check out the docs. As for myself, I’d like to play around more with Ktor before I’d commit to using it for a large scale production app - here are some topics I’d like to research before making that leap:

  • Explore ORM functionality via Ktorm (SQL) or KMongo (MongoDB).
  • Upgrade to Ktor 2.0.0 after Koin 3.2.0 is released from beta (specifically after this issue with Koin is resolved).
  • Figure out automatic Swagger doc generation from existing API code.

You can check out the full source code for this app on GitHub.