Kotlin

Primate compiles ordinary Kotlin route files with Kotlin/Wasm. Handlers may suspend, use Kotlin coroutines, access requests and sessions, and return Kotlin values directly. Production routes run on Node, Bun, and Deno.

For a first route and configuration, follow Add a Kotlin backend. The Kotlin SDK run.primate:kotlin is published on Maven Central.

Setup

Install the Primate adapter:

npm install @primate/kotlin

Use JDK 25 or later with current compatible Gradle and Kotlin releases. Configure a Kotlin Multiplatform wasmJs executable in build.gradle.kts:

@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)

plugins {
  kotlin("multiplatform") version "2.4.21"
}

layout.buildDirectory = file(".primate/kotlin-build")

kotlin {
  wasmJs {
    nodejs()
    binaries.executable()
  }

  sourceSets.wasmJsMain {
    kotlin.srcDir("routes")
    kotlin.srcDir(".primate-kotlin/src")
    dependencies {
      implementation("run.primate:kotlin:0.7.0")
      implementation(
        "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0",
      )
    }
  }
}

Add Maven Central to settings.gradle.kts, then enable the adapter:

import kotlin from "@primate/kotlin";
import app from "primate/app";

export default app({
  modules: [kotlin()],
});

Primate watches Kotlin routes and Gradle sources and compiles incremental changes into one application-wide Wasm module. Development builds use a managed helper; production builds and tests run Gradle normally.

For a nonstandard project layout, use inputs to specify watched paths and command, bootstrap, or artifacts to customize the build. Use developmentCommand and developmentArtifacts for development-specific overrides. Custom commands or artifacts use the command path unless you also configure developmentToolingCommand; set developmentTooling: false to use the command path directly.

Routes

Each .kt route declares a package and a main function that registers one or more handlers. The package must be unique within the application.

// routes/extended-route/Index.kt
package routes.extended_route

import run.primate.Route

fun main() {
  Route.get { "Hello from Kotlin" }
}

This file serves /extended-route. Route paths follow Primate's lowercase filesystem policy. For example, routes/users/[id]/Index.kt matches /users/42.

Route supports get, post, put, delete, patch, head, connect, options, and trace. Pass a content type before the handler when required:

Route.post("application/json") { request ->
  mapOf("body" to request.bodyText())
}

Requests

Handlers receive a Request with these properties and functions:

Route.get { request ->
  mapOf(
    "id" to request.path["id"],
    "search" to request.query["q"],
    "agent" to request.headers["user-agent"],
    "session" to request.cookies["session_id"],
    "pathname" to request.pathname,
  )
}

Missing bag entries return null. Query, path-parameter, and header lookup use Primate's case-normalized keys; cookie names remain case-sensitive.

Primate preserves request and transaction context when a handler resumes after Kotlin coroutine suspension or a host Promise.

Responses

See the cross-language response examples for equivalent Go, Python, Ruby, Kotlin, and JS/TS routes.

Return strings, booleans, numbers, lists, maps with string keys, or null. These become normal Primate responses. Keep Kotlin Long and ULong values within JavaScript's safe integer range.

The Kotlin SDK supports explicit abort, binary, error, JSON, null, redirect, SSE, text, view, and WebSocket responses. Response.Page(props, options) renders the page collocated with the current route, such as Index.svelte beside Index.kt. For collocated pages, register the frontend module (for example, @primate/svelte) after kotlin() so the .kt route handles the shared Index basename. SSE and WebSocket callbacks stay active while streams and sockets are open. The SSE callback infers its source type; when storing a source, use Response.SSE.Source. WebSocket callbacks use Response.WS.Actions, Response.WS.Message, and Response.WS.Socket.

Use the response types for explicit responses:

import run.primate.Response

Route.get { Response.Redirect("/login", 307) }
Route.post { Response.Error("Invalid input", 422) }
Route.get { Response.View("profile.svelte", mapOf("name" to "Ada")) }

Sessions

Session data is an ordinary Map<String, Any?>. Call session operations from a suspending handler.

Route.post { request ->
  request.session.create(mapOf("userId" to 42))
  request.session.get()
}

Available operations are create, get, tryGet, set, and destroy. They use Primate's configured session store and join an active transaction on the same database. Rolled-back lifecycle changes do not emit a session cookie.