Http

Using the browser's default fetch API can get quite tiresome, which is why fritz2 offers a small fluent api wrapper for it.

First, you create a Request which points to your endpoint url:

val swapiApi = http("https://swapi.dev/api").acceptJson().contentType("application/json")

The remote service offers some convenience methods to configure your API calls, like the acceptJson() above, which simply adds the correct header to each request sent via the template.

Sending a request is pretty straightforward:

swapiApi.get("planets/$num").body()

body() returns the body of the response as a String. Alternatively you can use the following methods to get different results:

  • blob(): Blob
  • arrayBuffer(): ArrayBuffer
  • formData(): FormData
  • json(): Any?

Or you can use the fritz2-serialization module, which contains a few helper functions for easy interoperability with kotlinx.serialization.

If your request was not successful (Response.ok property returns false according to the fetch API).

The same works for POST and all other HTTP methods - just use different parameters for the body to send.

The remote service is primarily designed for use in your Store's Handlers when exchanging data with the backend.

Here is a short example which uses fritz2-serialization module to handle the returning JSON:

@Serializable
data class Planet(val name: String)

val swapiStore = object : RootStore<String>("", job = Job()) {

private val api = http("https://swapi.dev/api")
.acceptJson()
.contentType("application/json")

val planetName = handle<Int> { _, num ->
val resp = api.get("planets/$num")
require(resp.ok)
resp.decoded<Planet>().name
}
}

For performance reasons the serialization module does not convert values to string using encodeToString and decodeFromString. Values are instead turned into "native" JavaScript objects using encodeToDynamic and decodeFromDynamic, which work natively with the Fetch API used by dev.fritz2.remote.

You can use this Handler like any other to handle Flows of actions:

render {
label {
+"Planet Name: "
swapiStore.data.render {
span {
+it
}
}
}
flowOf(1) handledBy swapiStore.planetName
// or just
swapiStore.planetName(1)
}

To see a complete example of this, visit our remote example.

In the real world, instead of creating the JSON manually, better use kotlinx.serialization. Get inspired by our masterdetail example.

You can easily set up your local webpack server to proxy services (avoid CORS, etc.) when developing locally in your build.gradle.kts:

kotlin {
js(IR) {
browser {
runTask {
devServer?.apply {
port = 9000
proxy?.apply {
put("/members", "http://localhost:8080")
put("/chat", mapOf(
"target" to "ws://localhost:8080",
"ws" to true
))
}
}
}
}
}.binaries.executable()
}

You can find a working example in the Ktor Chat project.

Want to do bidirectional communications? See Websockets.

Middleware

You can intercept calls made by the remote api, for example to implement cross-cutting concerns like logging, generic error handling, etc.

To write your own Middleware, implement the following interface:

interface Middleware {
suspend fun enrichRequest(request: Request): Request
suspend fun handleResponse(response: Response): Response
}

Make a Request by passing a Middleware to its use method:

val myEndpoint = http("/myAPI").use(someMiddleware)
myEndpoint.get("some/Path").body()

enrichRequest is called before each Request you configured to use this Middleware. You can add additional headers, parameters, etc., here. handleResponse is called on each Response.

To implement a simple logging Middleware, you could write the following:

val logging = object : Middleware {
override suspend fun enrichRequest(request: Request): Request {
console.log("doing request: $request")
return request
}

override suspend fun handleResponse(response: Response): Response {
console.log("getting response: $response")
return response
}
}

val myAPI = http("/myAPI").use(logging)

You can add multiple Middlewares in one row with .use(mw1, mw2, mw3). The enrichRequest functions will be called from left to right (mw1, mw2, mw3), the handleResponse functions from right to left (mw3, mw2, mw1).

You can stop the processing of a Response by Middlewares further down the chain by returning response.stopPropagation().

Authentication

In fritz2, you want to implement the authentication process of your SPA as a Middleware for its remote API. To do this conveniently, start by inheriting from

abstract class Authentication<P> : Middleware {

abstract fun addAuthentication(request: Request, principal: P?): Request

abstract fun authenticate()

}

fritz2's authentication allows you to specify the data type of the current authenticated user (principal). This could be, for example:

// This class holds the information of the principal currently authenticated
@Lenses
@Serializable
data class Principal(val name: String, val roles: List<String> = emptyList()) {
companion object
}

When you add this Middleware to your endpoint(s), it will intercept each response with status code unauthorized (401) or forbidden (403). You can change this by overwriting

override val statusCodesEnforcingAuthentication: List<Int> = listOf(401, 403, /* some more */)

Whenever the authentication middleware receives such a response, it starts the client-side authentication process you defined by implementing the abstract authenticate method. You are free to do here whatever your authentication process requires. For example, you could open a modal window to ask the user for their credentials and send them to another remote service to get a JSON Web Token for subsequent requests, as well as name and roles of the user. To successfully complete the authentication process with an identified principal, just call complete(someValidPrincipal). To cancel the running authentication process, call clear().

// This class holds the information entered in your login form
@Lenses
@Serializable
data class Credentials(val name: String = "", val password: String = "") {
companion object
}

object MyAuthentication : Authentication<Principal>() {
val loginStore = storeOf(Credentials(), job = Job())

val login = loginStore.handle {
val form = FormData()
form.set("username", it.name)
form.set("password", it.password)
try {
val principal = Json.decodeFromString<Principal>(http("/login").formData(form).post().body())
complete(principal)
closeTheLoginModal() // example
Credentials() // clear the input form
} catch (e: Exception) {
// show an error message
it
}
}

override fun authenticate() {
createSomeModal { // example
input {
loginStore.map(Credentials.name()).let {
value(it.data)
changes.values() handledBy it.update
}
placeholder("login")
}

input {
loginStore.map(Credentials.password()).let {
value(it.data)
changes.values() handledBy it.update
}
placeholder("password")
}

button {
+"login"
clicks handledBy login
}

}
}
}

Now you can use your principal's data to enrich each subsequent request by adding a token as a header, for example:

object MyAuthentication : Authentication<Principal>() {

//...

override fun addAuthentication(request: Request, principal: Principal?): Request =
if (principal != null) {
request.header("Authorization", "Bearer ${principal.token}")
} else request
}

You can also access the principal to control your user interface:

val vip = MyAuthentication.data.map { it?.roles?.contains("SomeVipRole") ?: false }

MyAuthentication.authenticated.render {
if (!it) {
button {
+ "login"

clicks handledBy {
MyAuthentication.start() // start the authentication process manually
}
}
}
else {
button {
+"logout "
MyAuthentication.data.map { it?.name ?: "" }.renderText()

clicks handledBy {
MyAuthentication.clear() // logout, but in real life you would want to inform the backend
}
}

button {
+ "only for VIP"
disabled(vip.map {!it} )
}
}

//....

}

If you have to access the current principal at any given point in time, you can do so using the current property of your Authentication.

If the first request requires authentication, subsequent requests that use the same authentication middleware will wait for the working authentication process to finish. So make sure you always complete or cancel it and use a fresh endpoint within for remote requests required (login, get roles, etc.).

Edit this page on Github