Skip to content

Repository files navigation

KtKit

Build Maven Central GitHub License GitHub commit activity GitHub issues Kotlin

A comprehensive Kotlin multiplatform toolkit for building server applications with Ktor.

📖 Documentation

🏠 Homepage (under construction)

Table of Contents

Overview

KtKit is a Kotlin multiplatform toolkit designed to speed up server-side application development with Ktor. It brings together several libraries into a cohesive set of tools that handle the repetitive aspects of backend development.

Note

Early Stage Project: KtKit is actively evolving. APIs may change between versions as we refine the abstractions based on real-world usage. Production use is possible but expect some breaking changes. Feedback and contributions are highly appreciated!

Usage

implementation("io.github.smyrgeorge:ktkit:x.y.z")

Ergonomics

The example module shows how Arrow's Raise and Kotlin context parameters keep handler and service code compact while preserving explicitness around errors and execution context.

A REST handler extends one of the handler base classes (here XRealNameRestHandler), declares its base path in uri() and its routes in routes(). Typed helpers (pathVariable, queryParam, body<T>()) parse the inputs, the return value is serialized as the response, and raised errors map to RFC 9457 ApiError responses. Handlers are auto-registered by the application (from TestRestHandler.kt):

class TestRestHandler(
    private val testService: TestService
) : XRealNameRestHandler() {
    override fun String.uri(): String = "/api/v1/test$this"

    override fun Route.routes() {
        GET("") {
            log.info { "Hello, ${user.username}!" }
            testService.withTransaction {
                testService.test().map { it.toDto() }
            }
        }
    }
}

The service the handler calls uses context parameters for the error channel (Raise<ErrorSpec>), the execution context, and the database scope (QueryExecutor/Transaction) (from TestService.kt):

class TestService(
    override val db: Driver,
    override val repo: TestRepository,
) : AuditableDatabaseService<Test> {
    val log = Logger.of(this::class)

    context(_: Raise<ErrorSpec>, _: QueryExecutor)
    private suspend fun findAll(): List<Test> = db { repo.findAll() }

    context(_: ExecContext, _: Raise<ErrorSpec>, _: Transaction)
    suspend fun test(): List<Test> {
        log.info { "Fetching all tests" }
        return findAll().also {
            log.info { "Fetched ${it.size} tests" }
        }
    }
}

The execution context is a coroutine context element that also carries log4k's tracing context:

class ExecContext(
    val reqId: String,
    val reqTs: Instant,
    val principal: Principal,
    val tracing: TracingContext,
    // Only a part of the context is presented here.
    // Check the documentation for more information.
) : TracingContext by tracing, CoroutineContext.Element

This lets handlers and services carry request metadata and tracing without threading parameters manually, while domain errors are raised through the Raise<ErrorSpec> context parameter. The context is propagated in two ways at once: via CoroutineContext and via context parameters in function signatures.

Modules and features

Gradle plugin (ktkit-gradle-plugin)

The Gradle plugin is the single entry point of a ktkit service build. A typical service build script (see the example module):

plugins {
    kotlin("multiplatform") // or kotlin("jvm")
    id("io.github.smyrgeorge.ktkit") version "x.y.z"
}

kotlin {
    jvm()
    macosArm64 { binaries { executable() } }
    // Include other targets as needed
}

ktkit {
    // Optional: database access via sqlx4k package
    sqlx4k {
        driver = PostgreSQL // also: MySQL, SQLite, SQLiteCipher
        generatedCodePackage = "io.github.smyrgeorge.ktkit.example.generated"
        extensions(Pgmq) // sqlx4k extensions; Pgmq (`ktkit-sqlx4k-pgmq`) is PostgreSQL only
        // Any sqlx4k code-generator option, applied last.
        // See the sqlx4k README for the full list.
        args = mapOf("expand-select-star" to "false")
    }
    // Optional: package the jvm target as a runnable, self-contained ("fat") jar (configures `jvmJar`).
    jar {
        mainClass = "io.github.smyrgeorge.ktkit.example.MainKt"
    }
}

The full list of options of each ktkit { } block is documented in its options file:

Block Options file Description
ktkit { } KtkitExtension.kt The top-level extension: common options (e.g. addDependencies) and the entry point of the blocks below.
jar { } JarOptions.kt Packages the jvm target as a runnable, self-contained ("fat") jar.
openApi { } OpenApiOptions.kt The compile-time OpenAPI generation (the ktkit OpenAPI compiler plugin). Enabled by default.
sqlx4k { } Sqlx4kOptions.kt Database access via sqlx4k: the driver, the generated-code package, and the code-generator arguments.

Application bootstrap and configuration

The Application wrapper is the entry point of a ktkit service: it manages the Ktor server lifecycle (startup/shutdown), sets up JSON and the Koin DI container, and auto-registers every REST handler bound as AbstractRestHandler. Tracing, logging, and DI are configured from one place:

Application(
    name = "ExampleApplication",
    conf = Application.Conf(host = "localhost", port = 8080),
    configure = {
        logging {
            // Configure logging.
            level = Level.INFO
            // Log in JSON format:
            // SimpleJsonConsoleLoggingAppender.install()
        }
        tracing {
            // Configure tracing.
        }
        json {
            // Configure JSON serialization.
        }
        ktor {
            // Additional Ktor configuration.
        }
        di {
            single { db }.bind<Driver>()
            singleOf(::TestRestHandler) { bind<AbstractRestHandler>() }
            singleOf(::TestService)
        }
    },
    postConfigure = {
        // After configuration, perform any necessary post-configuration tasks.
    }
).start()

A complete bootstrap lives in the example module: ExampleApplication.kt.

Kotlinx serialization

Everything that crosses the wire is a @Serializable class: REST request/response bodies, the RFC 9457 error responses, the TOML configuration, and the JSON database columns. There is no reflection involved, so serialization works identically on JVM and Native targets. The Gradle plugin applies the kotlinx.serialization compiler plugin automatically.

API errors (RFC 9457)

Domain errors are typed ErrorSpec values (e.g. NotFound, Unauthorized, Forbidden, MissingParameter, MalformedParameter, DatabaseError), raised through the Raise<ErrorSpec> context parameter instead of thrown. AbstractRestHandler maps each onto an RFC 9457 ApiError: httpStatus sets the status, the class name the title, message the detail, data() the data extension, the span id the requestId. Any other throwable is an UnknownError.

Security & permissions

Authentication and authorization are built into the request pipeline of AbstractRestHandler.

AuthenticationAbstractRestHandler takes a PrincipalExtractor, which resolves the authenticated Principal from the incoming request. When the extractor yields no principal, the handler falls back to its defaultUser (that is how AnonymousRestHandler works — it defaults to the anonymous principal); otherwise the request is rejected with a 401 Unauthorized. Ready-made handler base classes wire the extractor for you (e.g. XRealNameRestHandler uses XRealNamePrincipalExtractor).

The PrincipalExtractor implementations:

Extractor Source Description
XRealNamePrincipalExtractor XRealNamePrincipalExtractor.kt Decodes a base64-encoded JSON Principal from the x-real-name header, set by a trusted reverse proxy or API gateway.
BearerPrincipalExtractor planned JWT Bearer-token authentication (see the planned features).

Warning

The x-real-name mechanism is not safe to expose directly to the internet. It assumes a trusted reverse proxy or API gateway in front of the application that authenticates the user, strips any incoming x-real-name header, and sets it with the authenticated user's information before forwarding. Without such a proxy, any client could forge the header and impersonate any user.

Permissions — authorization runs after authentication in two layers, and both must pass (a failure responds with a 403 Forbidden API error):

  • Role-based — a Principal carries a set of roles; the handler constructor accepts hasRole, hasAnyRole and hasAllRoles constraints, enforced on every route of the handler.
  • Custom predicates — both the handler constructor and every route call accept a permissions: HttpContext.() -> Boolean function (handler-level and route-level predicates are combined with AND).
class AdminRestHandler : XRealNameRestHandler(
    hasRole = "admin", // Enforced on every route of the handler.
) {
    override fun String.uri(): String = "/api/v1/admin$this"

    override fun Route.routes() {
        GET("/reports", permissions = { user.hasRole("reports:read") }) {
            // ...
        }
    }
}

Logging, tracing, and metrics (log4k)

Observability is built on log4k — a multiplatform logging library with tracing and metrics. The ExecContext carries the request's tracing context end to end, so log lines and spans are correlated automatically across REST handlers, database transactions and queries, and PGMQ messages. The log4k annotations (@Traced, @Timed, @Logged) instrument service methods declaratively.

Health & metrics endpoints

Every application serves two unauthenticated status endpoints out of the box:

  • GET /api/status/health — application name, status, start time, and uptime
  • GET /api/status/metrics — the log4k metrics registry in OpenMetrics line format (VM memory and processor gauges are collected by default; register your own via log4k's Meter)

TOML Configuration Loading

ConfigPropertiesToml loads TOML files into @Serializable data classes, with environment-variable interpolation (${VAR_NAME}) and layered overrides: load() reads application.toml from resources as the base, then merges the first override found among application.toml, config/application.toml, application.local.toml and config/application.local.toml (override values win).

# src/commonMain/resources/application.toml
[database]
url = "postgresql://${DB_HOST}/mydb"
maxConnections = 10
@Serializable
data class AppConfig(val database: DatabaseConfig)

val config: AppConfig = ConfigPropertiesToml.load()

For the full API (loading from a specific file, explicit base/override merging), see ConfigPropertiesToml.kt.

Database support (sqlx4k)

Database access is built on sqlx4k — a coroutine-first SQL toolkit for Kotlin Multiplatform with compile-time query validation. PostgreSQL, MySQL/MariaDB, and SQLite are supported, on JVM and Native targets alike.

The ktkit-sqlx4k module glues sqlx4k into the toolkit: @Table entities with auto-managed audit columns (createdAt/ createdBy/updatedAt/updatedBy), @Repository interfaces implemented at compile time by the sqlx4k code generator, traced transactions and queries, error mapping into the ktkit error model, and JSON-column encoders for @Serializable classes. PGMQ (a message queue on Postgres) is available through ktkit-sqlx4k-pgmq.

Queue support (PGMQ)

Message-queue support is built on PGMQ — a lightweight message queue on Postgres, like AWS SQS and RSMQ — via sqlx4k's PGMQ client (PostgreSQL only).

The ktkit-sqlx4k-pgmq module glues it into the toolkit: the Pgmq wrapper and AbstractPgmqEventHandler, which propagates tracing and the authenticated user through message headers (in both directions — send attaches them, consuming restores them into a fresh ExecContext), and manages the consumer lifecycle with retries and graceful shutdown. Enabled via sqlx4k { extensions(Pgmq) } in the Gradle plugin.

OpenAPI generation (ktkit-compiler-openapi)

A Kotlin compiler plugin that generates the OpenAPI 3.1 specification of your REST handlers at compile time — no reflection, works on every KMP target (JVM and Native). It is attached automatically by the Gradle plugin (turn it off with ktkit { openApi { enabled = false } }).

At runtime the framework merges the generated fragments of all registered handlers and serves an interactive documentation UI — Swagger UI (default) or Scalar, selected via Application.Conf.OpenApi.ui — at GET /api/docs, and the merged OpenAPI 3.1 document at GET /api/docs/openapi.json.

  • What the plugin provides (analysis rules, @OpenApi metadata, configuration, limitations): ktkit-compiler-openapi

Example

Check the example application here.

Building & Development

Build

On a clean checkout (and after every version bump), bootstrap the build first — it publishes the ktkit Gradle plugin and the OpenAPI compiler plugin to mavenLocal, which the example module needs before the main build can even configure (see bootstrap.sh):

./scripts/bootstrap.sh

Then build as usual:

./gradlew build

Docker Setup

The project includes a docker-compose.yml for PostgreSQL:

docker-compose up -d

Contributing

This is an open-source project. Contributions are welcome!

License

Check the repository for license information.

Related Projects

  • log4k – Multiplatform logging with tracing
  • sqlx4k – Multiplatform database access

Author

Yorgos S. (@smyrgeorge)

About

A comprehensive Kotlin multiplatform toolkit for building server applications with Ktor.

Topics

Resources

Stars

40 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages