# Complete Guide to Becoming a Fullstack Kotlin Developer - Mobile and Web HTTP Client with KTOR

In this blog post, you will gain a comprehensive understanding of how to create a unified HTTP client for Android, iOS, and web applications within a Compose Multiplatform project. We will be utilizing the powerful KTOR library as our client. This guide will walk you through the entire process, from setting up your project to implementing the HTTP client, and will include detailed code examples and explanations to ensure you can follow along easily. By the end of this post, you will have the knowledge and skills to efficiently manage network requests across different platforms using a single codebase.

> The complete project is avaiable on [GitHub](https://github.com/mkonkel/GameShop)

---

## Initial setup

In the previous [post,](https://hashnode.com/post/clyl1s3ec000709la6vp34zzx) I covered common backend in which we decided that some objects can be shared across the app. Shared repository is a contract between a server and a client. We know exactly what methods are available and what data are served. Common objects also can save us a bit of time, there is no need to map them from one to another. It is time to use those objects in the frontend apps. First, we need to add the dependencies to the previously created modules in our shared module. 

`shared/build.gradle.kts`

```kotlin
sourceSets { 
    commonMain.dependencies { 
        implementation(projects.domain) 
        implementation(projects.repository) 
      } 
} 
```

Now we can start from we end of the last blog post by creating the implementation of the `GamesRepository` as the `HttpGamesRepository` in the common frontends code.

`HttpGamesRepository.kt`

```kotlin
internal class HttpGamesRepository(private val client: HttpClient) : GamesRepository { 
    override suspend fun getGames(): List<Game> { 
        return client.get(“/games”).body() 
    } 
}
```

If we take a good look at the previous backend code, it is almost the same as the `RealDatabaseRepository`. The only difference is that we need an **HttpClient** for the apps.

The most common multiplatform client is [KTOR](https://ktor.io/docs/create-client.html). It can be easily built into the KMM project without any additional code for each platform. It only needs a separate engine that can be provided via the dependencies. Following the documentation, we can create something like this.

> Please note that I am using the latest KTOR version with support for WASM, which is “3.0.0-wasm2”.

`libs.versions.toml`

```kotlin
[versions] 
ktor-wasm = "3.0.0-wasm2" 
 
[libraries] 
ktor-client-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor-wasm" } 
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-wasm" } 
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-wasm" } 
ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor-wasm" } 
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor-wasm" } 
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor-wasm" } 
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor-wasm" } 
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor-wasm" }
```

`shared/build.gradle.kts`

```kotlin
sourceSets { 
    commonMain.dependencies { 
	implementation(libs.ktor.client.core) 
	implementation(libs.ktor.client.logging) 
	implementation(libs.ktor.client.auth) 
	implementation(libs.ktor.client.content.negotiation) 
	implementation(libs.ktor.client.serialization.kotlinx.json) 
 
	implementation(libs.serialization) 
    } 
 
   iosMain.dependencies { 
       implementation(libs.ktor.client.darwin) 
   } 
 
   androidMain.dependencies { 
      implementation(libs.ktor.client.okhttp) 
   } 
 
   val wasmJsMain by getting { 
       dependencies { 
           implementation(libs.ktor.client.js) 
       } 
   } 
}
```

With the core dependencies added to the **commonMain** and the proper engines for the platforms, we can implement the client. The client can take the desired engine as a parameter to the `HttpClient` function, but if we leave it blank, the plugin will try to automatically provide a valid client based on the added dependencies – so for Android, it will be okHttp, and so on.

```kotlin
fun create(): HttpClient { 
    return HttpClient { 
        install(ContentNegotiation) { 
            json() 
        } 
        install(DefaultRequest) { 
            url("http://localhost:3000") 
            contentType(ContentType.Application.Json) 
            accept(ContentType.Application.Json) 
        } 
        install(Auth) { 
            bearer { 
                loadTokens { 
                    TODO() 
                } 
                refreshTokens { 
	    TODO() 
                } 
                sendWithoutRequest { request -> 
                    when (request.url.pathSegments.last()) { 
                        "login" -> false 
                        else -> true 
                    } 
                } 
            } 
        } 
    } 
} 
```

**ContentNegotiation** is a crucial component that handles the serialization and deserialization of requests and responses, ensuring that data is correctly formatted when being sent to and received from the server. This is especially important when dealing with JSON data, as it allows for seamless conversion between Kotlin objects and JSON strings.

With the **DefaultRequest** plugin, we can set up a default configuration for all outgoing requests. This includes specifying the base URL, which serves as the starting point for all relative URLs used in the client. Additionally, we can define common headers such as `Content-Type` and `Accept`, which inform the server about the type of data being sent and expected in return. This setup ensures that every request adheres to a consistent format, reducing the need for repetitive code.

The **Auth** plugin, combined with the **bearer** lambda, is responsible for managing authentication tokens. This plugin handles the acquisition and refreshing of tokens, which are essential for maintaining secure communication with the server. Within the **bearer** lambda, we define how to load the initial tokens and how to refresh them when they expire. This involves specifying functions that will be called to retrieve new tokens, ensuring that the client always has valid credentials.

Moreover, we can configure the **Auth** plugin to automatically add tokens to certain requests. The `sendWithoutRequest` function allows us to specify which endpoints should bypass token addition. For instance, in our setup, the `/login` endpoint is publicly accessible and does not require tokens. By returning `false` for this endpoint, we ensure that the client does not attempt to add tokens or handle 401 Unauthorized responses for login requests.

In scenarios where the server responds with an HTTP 401 Unauthorized status, the client is designed to automatically attempt to refresh the token. If successful, the client will retry the original request with the new token, thereby maintaining a seamless user experience without requiring manual intervention.

In our specific case, the `/login` endpoint is the only publicly available endpoint. This endpoint is responsible for providing a valid access token and user data upon successful authentication.

## Handling Authentication

The lambda requires a `BearerTokens` object, which consists of two string values `accessToken` and `refreshToken`. If we want to be flexible, we need to store a token after every successful login and add it to every subsequent requests.

```kotlin
internal interface TokenStorage { 
    fun putTokens( 
        accessToken: String, 
        refreshToken: String, 
    ) 
 
    fun getToken(): BearerTokens 
} 
```

The implementation is as simple as it can be the values are stored in the `mutableList`.

```kotlin
internal class RealTokenStorage : TokenStorage { 
    private val tokens = mutableSetOf<BearerTokens>() 
 
    override fun putTokens( 
        accessToken: String, 
        refreshToken: String, 
    ) { 
        tokens.add(BearerTokens(accessToken, refreshToken)) 
    } 
 
    override fun getToken(): BearerTokens { 
        return tokens.last() 
    } 
}  
```

The updated `HttpClient` looks like this:

```kotlin
internal class HttpClientFactory( 
    private val tokenStorage: TokenStorage, 
) { 
    fun create(): HttpClient { 
        return HttpClient { 
            ... 
            install(Auth) { 
                bearer { 
                    loadTokens { 
                        tokenStorage.getToken() 
                    } 
                    refreshTokens { 
                       TODO(“Not implemented uet”) 
                    } 
                    ... 
                } 
            } 
        } 
    } 
}
```

> Please notice that we don’t implement the refresh token mechanism as this is not important for the matter of the post. 

At the beginning, let's delve into the implementation details of the `HttpGamesRepository`. This repository is responsible for fetching game data from a remote server. However, before we can retrieve any game information, we need to authenticate the user and obtain a valid token. To handle this, I have introduced a `LoginRepository` along with its implementation.

```kotlin
interface LoginRepository { 
    suspend fun login(username: String, password: String): LoginResponse? 
}
```

```kotlin
internal class HttpLoginRepository( 
    private val client: HttpClient, 
    private val tokenStorage: TokenStorage, 
) : LoginRepository { 
    override suspend fun login(username: String, password: String ): LoginResponse? { 
        val request = LoginRequest(username, password) 
 
        return client.post("/login") { setBody(request) }.body<LoginResponse?>() 
            .also { 
                if (it != null) { 
                    tokenStorage.putTokens(it.token, "NOT IMPLEMENTED") 
                } 
            } 
    } 
} 
```

Making requests with **KTOR** is simple. Using the provided **login** and **password**, we create a `LoginRequest`, and then we use the injected client (which has a base URL and headers configured by default).

We need to specify the **method** and **path** for our request. Then, in the lambda builder block, we add the **body** of the request and that's it - the request is ready.

To obtain the body of the response, we use a typed function that tries to receive the **JSON** and **deserialize** it to a given type.

To wrap things up and make everything work, it is very helpful to use a **DI** framework like KOIN. Unfortunately, at the time of writing this post, KOIN does not support the **wasm** target. In such cases, we need to create our own way of injection.

> *When the blog post was published the KOIN added support for the* ***wasm****. Feel free to use it. I will do my best to update the project and add some explanations as soon as possible.* 

```kotlin
object DI { 
    private val tokenStorage: TokenStorage = RealTokenStorage() 
    private val httpClientFactory: HttpClientFactory = HttpClientFactory(tokenStorage) 
    val remoteRepository: RemoteRepository = RealRemoteRepository(httpClientFactory.create(), tokenStorage) 
}
```

I’ve added a factory for the remote repositories which will aggregate all the `HttpRepositories` in one class for simplicity. 

```kotlin
interface RemoteRepository { 
    fun loginRepository(): LoginRepository 
    fun gamesRepository(): GamesRepository 
}
```

```kotlin
internal class RealRemoteRepository( 
    private val client: HttpClient, 
    private val tokenStorage: TokenStorage, 
) : RemoteRepository { 
    override fun loginRepository(): LoginRepository = HttpLoginRepository(client, tokenStorage) 
    override fun gamesRepository(): GamesRepository = HttpGamesRepository(client) 
}
```

With all the work done, we can use our **DI** and write a quick test to see if logging and fetching the games works. We can use the `jvmTest` module to hold tests for the standard code. To do so, we need some dependencies that will allow us to manage tests – `kotest`, `JUnit`, `coroutines`, and a valid client for the **JVM** target (we can use `KTOR` `CIO` or `OkHttp`). 

`libs.versions.toml`

```kotlin
[versions] 
junit = "4.13.2" 
kotest = "5.8.0" 
coroutines = "1.8.0-RC2" // wasm support 
 
[libraries] 
junit = { group = "junit", name = "junit", version.ref = "junit" } 
kotest-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } 
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } 
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
```

`shared/build.gradle.kts`

```kotlin
jvmTest.dependencies { 
    implementation(libs.ktor.client.okhttp) 
    implementation(libs.kotest.core) 
    implementation(libs.kotest.property) 
    implementation(libs.junit) 
    implementation(libs.coroutines.test) 
} 
```

With the dependencies added we can write a simple repositories test. 

```kotlin
class HttpRepositoriesTest { 
    private val loginRepository = DI.remoteRepository.loginRepository() 
    private val gamesRepository = DI.remoteRepository.gamesRepository() 
 
    @Test 
    fun `login with valid credentials`() = runTest { 
        val result = loginRepository.login("admin", "pass") 
 
        with(result.shouldNotBeNull()) { 
            token.shouldNotBeNull() 
            user.shouldNotBeNull() 
        } 
    } 
 
    @Test 
    fun `should return all games`() = runTest { 
        // login first to get the token and store it in the token storage 
        loginRepository.login("admin", "pass") 
 
        val result = gamesRepository.getGames() 
 
        result.shouldNotBeEmpty() 
    } 
} 
```

To sum things up, we have successfully set up a **Kotlin multiplatform** project that includes a fully functional **KTOR** backend and a common **KTOR** client capable of communicating with the running server. This setup allows us to send and fetch data seamlessly. The next step in our development process is to present this data to the application users in an intuitive and efficient manner.

Achieving this requires addressing several architectural tasks. First, we need to create a presentation layer that will display our data and manage user interactions. Additionally, we must implement a navigation system to guide users through different sections of the app, ensuring a smooth and coherent user experience.

In the upcoming blog post, we will delve deeper into the Mobile and Web Application Architecture using Decompose. We will explore how to structure the application, manage state, and handle navigation effectively. Stay tuned for a comprehensive guide that will help you build robust and scalable applications.

> This blog post provides a detailed guide on creating a unified HTTP client for Android, iOS, and web applications using Compose Multiplatform and the KTOR library. You'll learn how to set up your project, implement the HTTP client, and handle authentication tokens. The post includes step-by-step instructions, code examples, and explanations to help you manage network requests efficiently across multiple platforms using a single codebase.
