Why I use Kotlin/JS and Express to build websites instead of Ktor

2025-08-02
People freak out when I tell them that I build my websites using Kotlin/JS, instead of Kotlin/JVM. People freak out even more when I tell them that I build on top of Express.js instead of Ktor for my server. In this post, I wrote down all the reasons why my favorite Kotlin target is `Kotlin/JS` and why I don't use Ktor for my startups.

Trying to build web startups as an Android Engineer

A few years ago, I quit my SWE job to build my own business. I used to work as a mobile engineer for over a decade and I hadn't touched any server stuff since my uni days.

The first startup I built was built using PHP, because we used it in my uni days. It was incredibly humbling because I had to drop all my skills and start from zero to build something super basic. Had to understand how the web worked to understand what I was dealing with. It took me a month to build, but I was able to launch it!

For my second startup, I tried to use my existing skills and try Kotlin. Thankfully, there is an official Kotlin server framework called Ktor. With my limited knowledge of the Web, I was quickly defeated. Nothing made sense, the documentation was very limited, and it was in no way web-beginner friendly. I think that was because people use Kotlin on the backend to migrate from Java, and they are already familiar with other JVM frameworks such as Spring Boot.

I remember spending a few weeks trying Ktor and building apps with it, but I just couldn't do it. LLMs were not a thing either, and anywhere I would go I just couldn't get the level of support I needed.

Being defeated by my two attempts, I decided to try out a technology that was popular enough so I could get as much support as I could, with a language that was modern enough so that I could (somewhat) reuse my Kotlin skills. That's how I ended up using JavaScript and Next.js.

Learning Web Development using JavaScript and Next.js

Building sites with Next.js felt very familiar coming from Android. It had a very particular way of structuring your project, building web pages, and everything was thought out for you. For building UI, it uses React, which I believe is what inspired Jetpack Compose, so it felt very familiar on that end as well.

At that point, I had learned about Tailwind CSS which made using CSS a bit closer to using Compose:

// Jetpack Compose Component
@Composable
fun Card() {
    Column(
        modifier = Modifier
            .padding(16.dp)
            .widthIn(max = 320.dp)
            .shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp))
            .clip(RoundedCornerShape(12.dp))
            .background(Color.White)
            .border(width = 1.dp, color = Color(0xFFE5E7EB), shape = RoundedCornerShape(12.dp))
            .padding(24.dp)
    ) {
        Text(
            text = "Card Title",
            fontSize = 20.sp,
            fontWeight = FontWeight.SemiBold,
            color = Color(0xFF1F2937),
            modifier = Modifier.padding(bottom = 8.dp)
        )
        Text(
            text = "This is a subtitle that provides additional context and information about the card content.",
            fontSize = 14.sp,
            color = Color(0xFF6B7280),
            lineHeight = 21.sp
        )
    }
}

vs

// Next Js Component

export function Card() {
    return (
        <div className="flex flex-col bg-white rounded-xl p-6 shadow-md border border-gray-200 max-w-sm m-4">
            <h2 className="text-xl font-semibold text-gray-800 mb-2">
                Card Title
            </h2>
            <p className="text-sm text-gray-500 leading-relaxed">
                This is a subtitle that provides additional context and information about the card content.
            </p>
        </div>
    );
}

There were also way more people talking about JavaScript online on platforms like Stack Overflow (rest in peace), Discord, and Twitter, so getting your questions answered was much more straightforward.

The other great thing about the JavaScript ecosystem is how rich it is. There is a package for everything: simple file system access, parsing Markdown, data visualization, authentication, highlighting code, cron jobs, and so on.

Using Next.js was great for learning how to build websites, but you end up hitting limits quite fast. Next.js is meant to be a frontend framework. This means that you don't have the option of using the filesystem to save stuff (such as databases or even simple logs) and you are forced to use a third-party service (such as Firebase).

At first, I thought I was saving time by using a third-party service for my authentication and database, but this ended up being the opposite of the truth. Saving something to the file system is as easy as it can get. Using such services, you end up overcomplicating your life for no real benefit – your business depends on a third party that might be down at any point, your app gets slower as it needs to talk to a remote server to fetch data. Also, such services make it surprisingly hard to browse and query your data.

Ditching the training wheels – Building a real server

After having an understanding of how the Web works, and having built a few websites, I wanted to build a server from scratch again. At this point, I was tired of the limitations of using frameworks and I just wanted complete control.

Your software can be as simple or as complex as you make it. The same thing goes for building a server. It might sound complex, but it really isn't, especially if you take things one at a time.

For example, here is what a server looks like using Express.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('<h1>Hello World!</h1>');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Not scary at all, right? Even if you don't know anything about servers, you can understand that it returns some basic HTML at route "/" and it runs on port 3000.

Not only is this very simple to use, it is also very, very flexible.

Express.js has been around for ages, so the ecosystem and support are huge (great selection of plugins such as SSO authentication). Plus, the fact that this runs from a JavaScript script (it is really a simple index.js file) gives you full control of what you can run (as it should – it is code after all!).

This time I went as simple as possible and kept everything on the server. I used SQLite for my database (from my Android days) and found out about phpLiteAdmin which sets up a basic dashboard to browse and query your database.

For hosting purposes, I decided to self-host using a VPS. I ended up using Digital Ocean as I had heard of it a million times before knowing what a server was. Plus, their dashboard is straightforward to use and seems developer-focused, so I was happy (still a happy customer to this date).

Back to Kotlin (with a twist)

I had finally figured out a way that I could build servers comfortably. I could create web pages, endpoints, cron jobs, you name it. There was one thing missing.

JavaScript was still as close to Kotlin as possible, but it was still not Kotlin. I found myself having to reinvent the wheel far too many times using JavaScript (inb4 skill issue):

Want to find an item in a list?

Want to filter items?

Want to dynamically create strings (which you do a lot on the web if you return HTML)?

Kotlin has this nice standard library that does everything you need in realistic scenarios. JavaScript is just barebones. I am aware of libraries such as Lodash that try to be a standard library, but at the end of the day, I wanted to use Kotlin because that's the language I am super productive with.

Also, you see, I work a lot with Compose UI. As a matter of fact, I built a rather popular website and library about Compose because of how much I like it. For my native apps I use Compose given that I can use Kotlin and one of the best (imho) UI frameworks out there.

Ideally I want to use only one language (Kotlin) to do everything (native and web apps). Not only because I prefer Kotlin as a language, but the context switch between different languages is way too big and is slowing me down a lot.

So I started experimenting with Kotlin/JS. I was curious to see if I could use Kotlin from Javascript same way as you can call Kotlin from Java, and it worked!

It worked, but the interop was not as straightforward. Ended up spending far too much time on trying to link the two worlds together, which ended up breaking all the time.

So, after some good thought, I decided to just ditch JavaScript entirely and just use Kotlin/JS entirely to use JavaScript packages.

Here is what the earlier Express app looks like in Kotlin:

@JsModule("express")
@JsNonModule
external fun express(): dynamic

fun main() {
    val app = express()

    app.get("/") { req, res ->
        res.send("<h1>Hello World (from Kotlin)!</h1>")
    }

    val port = 3000
    app.listen(port) {
        console.log("Server running at http://localhost:$port")
    }
}

Looks very similar, doesn't it? What's great about this is that you can use Kotlin while using the entirety of the JavaScript ecosystem. I haven't hit any limitations after using it for a few months.

Why Kotlin JS is my favorite Kotlin target

I didn't expect to like Kotlin/JS this much. It combines what I like about Kotlin and what I love about JavaScript in one power tool: pragmatism and prototyping.

Here is the actual way I do Markdown parsing. It uses gray-matter, a popular YAML parser package for JavaScript, to parse Markdown files and return a simple data class with the parsed result:

import com.alexstyl.filesystem.Filesystem
import com.alexstyl.filesystem.readFile

@JsModule("gray-matter")
@JsNonModule
external val matter: (String) -> GrayMatterFile

external interface GrayMatterFile {
    val data: dynamic
    val content: String
}

data class ParsedMarkdownFile<T>(val content: String, val frontMatter: T)

fun <T> parseMarkdown(markdown: String): ParsedMarkdownFile<T> {
    val grayMatterFile = matter(markdown)
    val grayData = grayMatterFile.data
    val frontMatter = grayData.unsafeCast<T>()

    return ParsedMarkdownFile(
        frontMatter = frontMatter,
        content = grayMatterFile.content
    )
}

suspend fun <T> Filesystem.readMarkdown(path: String): ParsedMarkdownFile<T> {
    val text = Filesystem.readFile(path)
    return parseMarkdown(text)
}

Now you might be wondering "so what?". We get to turn off types when we don't need them!

Notice the dynamic type in the GrayMatterFile:

external interface GrayMatterFile {
    val data: dynamic
    val content: String
}

dynamic is a special type on the Kotlin/JS target. It is not like Any that just marks the type as 'anything goes'. dynamic lets you call things on it without guarantees.

This means that the following is legit from a compiler standpoint:

 val data : dynamic = // the data from before
 
 data.randomValueCall

 data.callOtherFunction()

We are telling the compiler to turn off type checks for this object and that we'll take it from here. This lets you move insanely fast while prototyping apps. It saves me time from having to create @Serializable data classes or being forced to figure out types when the return is too trivial (i.e., trying out a library or API to see if it is actually useful to me).

From the call site, the code looks like this:

external interface BlogPostFrontMatter {
    val title: String
    val date: Date?
    val excerpt: String?
    val tags: Array<String>?
    val social_image: String?
    val draft: Boolean?
}

val blogPostFrontMatter = Filesystem.readMarkdown<BlogPostFrontMatter>("path/to/markdown.md")

I love Kotlin's type system, but only when it relates to the domain of my app. I don't want to have to make the compiler happy just so that I can move forward.

Combining HTML with Compose Web

At this stage, I have decided to stop building using React in favor of embedding Compose Web, and completely remove any interactivity on my sites with just plain HTML and CSS (no JavaScript or jQuery).

An early example of this is my SVG to Compose Converter. It is a simple web page that hosts a Compose app. Web indexable stuff are in plain HTML (Kotlin/JS Express) while the mini app is in Compose Web.

A more sophisticated example of this can be found on the Compose Unstyled live demos. You get the full SEO juice of a documentation website while having the full interactivity of Compose. It doesn't get any better than this.

For static content (HTML) I use kotlinx.html together with Tailwind CSS like this:

div("max-w-7xl mx-auto") {
    div("flex flex-col ") {
        AppBar(
            title = {
                h1("text-xl font-semibold text-gray-800") {
                    +"My Website"
                }
            }
        )
    }
    div("mb-8") {
        h1("text-3xl font-bold text-gray-900 mb-2") {
            +"Welcome back"
        }
        p("text-gray-600") {
            +"Here's what's happening with the world today"
        }
    }
}

which allows me to build my own components when needed like this:

typealias Html = FlowContent

fun Html.AppBar(
    navigation: (Html.() -> Unit)? = null,
    title: (Html.() -> Unit)? = null,
    actions: (Html.() -> Unit)? = null,
    className: String = "",
) {
    nav("h-[var(--app-bar-height)] p-2 w-full flex justify-between $className") {
        div("flex gap-2 items-center") {
            if (navigation != null) {
                navigation()
            }
            if (title != null) {
                title()
            }
        }
        if (actions != null) {
            div("flex items-center gap-2") {
                actions()
            }
        }
    }
}

Turns out Compose is not just a framework but a mental model for working with UI.

Want to discuss this article? Join the discussion on X ->

I'm Alex Styl
Used to build mobile apps at Apple but then I quit to build my own business. These days I spend most of my time in Asia building products. Leave your email if you want to follow my stories, or follow me on X.