Sharing Data Objects Between Frontend and Backend
If you start working on a fullstack project, you might find yourself experimenting during the prototype phase by passing raw strings back and forth between your client and server. However, you'll likely eventually want to send rich data objects instead, as they are more suited to encapsulating complexity while also providing type safety.
Thanks to Kotlin multiplatform and Kotlinx serialization, this is easy to do!
Overview
In summary, you will need to:
- Add dependencies on
kotlinx.serialization
and (optionally)kobwebx.serialization.kotlinx
to your site's build script. - Create a
commonMain
folder which is where your data objects will live. - Add backend and frontend code that serializes and deserializes these objects before sending them over the network.
To see this in action, we'll demonstrate implementing a single request / response object pair. To keep it simple, the request will contain a string that should be sent to the server and echoed back to the client in the response object. To make the request a little more interesting, we'll also include an operation instruction that the server should perform on the string before sending it back.
If you're not familiar with the request / response pattern, it is a common way to structure an API for services that communicate over a network. When you stick with it, the convention is easy to read and understand quickly, and the approach is generally conducive to backwards compatibility when you inevitably need to add new fields later. (Writing backwards compatible data objects is outside the scope of this guide, however.)
Build script
gradle/libs.versions.toml
site/build.gradle.kts
Common code
site/src/commonMain/kotlin/com/example/models/Echo.kt
Backend code
We'll be designing this API as a POST request, even though in this case it is stateless and using GET might more reasonably be considered suitable. This is because, in practice, when you send request objects to a server, you commonly want to add, update, or delete some state on the server.
But more importantly, HTTP post requests support including a body, which is the natural place to embed rich data objects; GET requests do not. (We'll show how to handle GET requests as well shortly.)
site/src/jvmMain/kotlin/com/example/api/Echo.kt
Hopefully the endpoint is fairly self-explanatory. We parse the request body and, based on the operation, populate the response body.
Request.readBody
and Response.setBody
are convenience methods that are provided by the com.varabyte.kobwebx:kobwebx-serialization-kotlinx
artifact we included earlier.
You could also write ctx.req.readBodyText()?.let { text -> Json.decodeFromString<EchoRequest>(text) }
and ctx.res.setBodyText(Json.encodeToString(echoResponse))
if you don't mind the verbosity and want to skip adding the dependency.
If you really want to pass a data object with a GET request, you can use query parameters for that:
Frontend code
With our endpoint defined, we can now trigger it from the frontend. In a @Page
somewhere, you'll want to call one of the window.api
HTTP methods that can take and serialize a body argument.
As the window.api
methods are all suspend functions, one of the easiest ways to call them (and thus demonstrate one) is from a LaunchedEffect
block:
That's it!
If you are getting compile errors with the above code, be sure to import the relevant extension method. For the above example, this would be import com.varabyte.kobweb.browser.post
.
In practice, you'll probably use something like rememberCoroutineScope
and use scope.launch
in response to some user event, such as clicking a button:
If you want to pass a data object along with a GET request, instead, you can embed it as a query parameter:
Characters will automatically be URL-encoded when you do this.
Final thoughts
Thanks to Kotlin Multiplatform and Kotlinx serialization, it's easy to share data objects between the frontend and the backend of a Kobweb fullstack project.
Historically, sharing data has been a pain point for fullstack developers. A common solution is to reach out to a solution like Protocol Buffers, which is a way to declare data values in a language-agnostic manner and then use a custom protobuf compiler to convert that declaration to target code you care about.
If you have to support a client and server that use different languages, then this is still a reasonable approach, but if you can use Kotlin on both ends, then the approach discussed in this guide is much simpler.