Building a Back Office with Kotlin JS & Firebase

Now legitimately an entrepreneur!

I’ve recently decided to invest in a small local company with a good college buddy of mine. The company rents out containers for construction & demolition waste pickups and offers a junk removal service.

Now, when my friend bought the company a couple of years back, it was a mom-and-pop shop. The company back office he inherited was a bunch of Google Sheets, involving many manual entries with a copious amount of copy-pasta.

It worked “fine”, but the company’s day-to-day administration quickly got tedious with those kinds of tools, as you can imagine. Since my primary expertise is developing software, my role in the company will be to build a better back-office.

Enter: Kotlin-JS & Firebase

Motivations

My first project was to rebuild the company’s client-facing website. After a good amount of exploration, we settled with a template generator for bilingual support and Bootstrap to keep styling simple.

After rebuilding the site’s static content, we turned our attention to client onboarding. We had good results using a simple Firebase realtime DB setup here.

This brings us to the back-office project. Having used it for our client-facing website, I knew Javascript’s weak typing approach was going to make me miserable down the line. Coming into this project as an Android dev with a lot of Kotlin experience, Kotlin-JS felt like something I should explore.

Project build basics

Before we get started, you can follow along by checking out my blog-samples github repo. You’ll find the code backing all my kotlin-js examples under the kotlin-js project folder.

Now, to keep things simple, let’s start this project as a “module-less” build.

.
├── build.gradle.kts
├── settings.gradle.kts
├── src
│   ├── main
│   │   ├── kotlin
│   │   │   └── ...
│   │   └── resources
│   │       ├── index.html
│   │       └── ...
│   └── test
│       ├── kotlin
│       └── resources
└── webpack.config.d

The downside with a single-module project being, it doesn’t promote code reuse. But for a small project, being nimble with the build is more important IMO.

Let’s take a look at our build.gradle.kts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
    kotlin("js") version "1.4.31"
    kotlin("plugin.serialization") version "1.4.31"
}

repositories {
    mavenCentral()
    maven { url = uri("https://dl.bintray.com/kotlin/kotlinx") }
    maven { url = uri("https://dl.bintray.com/kotlin/kotlin-js-wrappers/") }
}

dependencies {
   implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2")

	// ...
}

kotlin {
    js { 
        browser {
        	// ... 
        }
    }
}

The baseline here is:

  • kotlin("js"), automatically provides us with the core js dependencies needed for everyday work in Kotlin JS.
  • The kotlinx-html-js dependency gives us a DSL to build our HTML.

But how do we get access to the broader Javascript library ecosystem?

This project will be using Firebase and Bootstrap 4, bringing them via npm. We can add these dependencies to our Gradle as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dependencies {
	// ... 
    implementation(npm("firebase", "^8.3.1"))
    implementation(npm("firebaseui", "^4.8.0"))

    implementation(npm("bootstrap", "^4.6.0"))
    implementation(npm("bootstrap-icons", "^1.4.0"))
    implementation(npm("jquery", "^3.6.0"))
    implementation(npm("@popperjs/core", "^2.9.1"))
}

Under the hood, the Kotlin JS build plugin uses the webpack module bundler. Above, we use the npm() function to declare those dependencies.

When we’re ready to start developing our project, we’ll use the :browserDevelopmentRun Gradle target. What’s nice here is this automatically re-deploys the changes we make to our project and lets us test them live in a browser.

When we’re ready to deploy, we execute the build target and publish the .js and .html files generated under ~/build/distributions to our hosting solution of choice.

Our “host”, resources/index.html

We’ll need a simple host .html file for our kotlin-js code to ‘live in’.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Host page</title>
    <script defer src="kotlin-js-samples.js"></script>
</head>
<body style="padding-top: 75px">
</body>
</html>

We import the kotlin-js-samples.js script, as configured in the sample project for this article. We’ll be including .css styles and npm libraries straight from the Kotlin-js source code. You’ll also want to have your DOM-related work done programmatically, index.html only acting as a host for our Kotlin code.

Starter function fun main()

Let’s start coding. Our starting point is your typical main() function. Our project uses Bootstrap and Firebase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun main() {
    // Bootstrap CSS
    require("bootstrap/dist/css/bootstrap.css") // 1️⃣ 

    // Firebase dependencies. Note the use of `dynamic` type below...
    val firebase: dynamic = require("firebase/app").default // 2️⃣
    require("firebase/auth")
    require("firebase/database")
    
    val firebaseConfig: Json = json(
	    // Add needed Firebase configuration values here, format being: 
	    "key" to "value"
    )
    firebase.initializeApp(firebaseConfig) // 3️⃣
    
    // ...
}

1️⃣ By calling require("bootstrap.css"), we pull in that CSS to make it available in our project. It ends up bundled in kotlin-js-samples.js, and made available to our generated DOM.

2️⃣ Next up is initializing the Firebase library. Note the use of the dynamic keyword. That keyword tells Kotlin we’re dealing with a loosely typed javascript object. With a dynamic variable, Kotlin allows us to call any property or function. This is how we’ll interact with Firebase during initialization. We also call require() on auth and database, the two components of Firebase we’ll be using for now.

3️⃣ Finalize everything with a call to firebase.initializeApp() with the appropriate configuration Json object.

Typing Javascript objects

Now, dynamic is a nice shortcut to interface with the Javascript world. But the main attraction in Kotlin is the type system. So how do we bridge the two worlds? Let’s say we want to declare a Database Kotlin type, matching the existing Firebase Javascript Database type. This is possible with the external keyword:

1
2
3
4
5
6
7
external interface Database {
   fun useEmulator(host: String, port: Number)
   fun goOffline(): Any
   fun goOnline(): Any
   fun ref(path: String = definedExternally): Reference
   fun refFromURL(url: String): Reference
}


By explicitely mapping out the Database Javascript API in a way Kotlin understands, we can then use it to assign a type to Javascript objects.

1
val fbDb = firebase.database() as Database

Having assigned a type to the fbDb object, we now get the full Kotlin typing system benefits. That’s pretty cool, but… defining all these types… that’s going to be a lot of work, right? Ugh 🙅‍♂️…

dukat gradle tasks

Thankfully, it’s work that the Typescript community has already done for us. With Typescript, .d.ts declaration files exist for most libraries that are part of the Javascript ecosystem. These declaration files do much the same thing as our external definition above. They add type information onto existing Javascript libs.

Using a tool called dukat, we can take .d.ts TypeScript declaration files and generates Kotlin external declarations automatically.

Now, it’s not all 🌞 and 🌈 just yet. Dukat, as of writing, is pretty much still a beta tool, and the generated files don’t always work out 100%. For Firebase, I ended up having to massage the generated content a bit to get it to work to my tastes.

Bottom line, ✨feels like magic✨, not sorcery 🧙‍♂️. You can take a peek at dukat’s edited end result on this blog’s github repo.

Fetching from Firebase

Let’s close today’s article with a look at how to fetch some data from Firebase with our setup. Let’s setup a read-only Firebase realtime DB for this example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "rules": {
    ".read": false,
    ".write": false, 
    "customers": {
      ".read": true, 
      ".write": false,
      "$cUid": {
        "contact": {
          "firstName": {".validate": "newData.isString()"},
          "lastName": {".validate": "newData.isString()"},
          "email": {".validate": "newData.isString()"},
          "phone": {".validate": "newData.isString()"},
          "$anyOther": {".validate": false}
        },
        "address": {
          "address1": {".validate": "newData.isString()"},
          "address2": {".validate": "newData.isString()"},
          "city": {".validate": "newData.isString()"},
          "postalCode": {".validate": "newData.isString()"},
          "$anyOther": {".validate": false}
        },
        "$anyOther": {".validate": false}
      }
    }
  }
}

This ruleset only allows reads on existing stored data. Let’s define proper Kotlin types, to match this structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
external open class Customer {
    val contact: Contact?
    val address: Address?
}

external open class Contact {
    var email: String?
    var firstName: String?
    var lastName: String?
    var phone: String?
}

external open class Address {
    var address1: String?
    var address2: String?
    var city: String?
    var postalCode: String?
}

You might wonder why all the listed fields are nullable. While the validation rules somewhat enforce typing, there’s no easy way to prohibit null or undefined fields under Firebase realtime DB. So we have to consider that when fetching data. That said, nothing stops you from converting Firebase objects into more formal data class types.

Now, here’s an example of how you can fetch objects from the Firebase realtime DB:

1
2
3
4
5
6
7
8
database.ref("customers/$key")
    .once("value") { snap, _ ->
        val customer = snap.`val`().unsafeCast<Customer>()
        console.log("Customer: %o", customer)
    }
    .catch { throwable ->
        console.error("Error on fetchCustomer(): %o", throwable)
    }

The database.ref() call points to the data we want on the server. We call once() with the "value" parameter and a handler. That handler will be called exactly once, with the current data snapshot for this reference. The once() call itself returns a Promise<DataSnapshot>, so we can chain a .catch {} call to it in case any problems come up. Docs for once() and Reference can be found here.

Whats next?

There’s a lot to explore with kotlin-js. The plan is to next focus on some UI examples with kotlinx.html and dive into Flow with coroutines usage.

I’m also pretty interested in getting feedback. Anything you’d like to see first? Questions, deep-dive requests, ideas for improvements? DM me on Twitter, and we’ll chat.

Take care, folks!