
Accelerates server-side development with type-safe REST, database and message-queue abstractions, built-in structured logging/OpenTelemetry tracing, dependency injection, config/context propagation, and functional Either-based error handling.
A comprehensive Kotlin multiplatform toolkit for building server applications with Ktor.
🏠 Homepage (under construction)
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!
implementation("io.github.smyrgeorge:ktkit:x.y.z")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.ElementThis 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.
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. |
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.
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.
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.
Authentication and authorization are built into the request pipeline of AbstractRestHandler.
Authentication — AbstractRestHandler 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-namemechanism 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 incomingx-real-nameheader, 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):
Principal carries a set of roles; the handler constructor accepts hasRole, hasAnyRole and
hasAllRoles constraints, enforced on every route of the handler.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") }) {
// ...
}
}
}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.
Every application serves two unauthenticated status endpoints out of the box:
GET /api/status/health — application name, status, start time, and uptimeGET /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)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 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.
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.
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.
@OpenApi metadata, configuration, limitations):
ktkit-compiler-openapi
Check the example application here.
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.shThen build as usual:
./gradlew buildThe project includes a docker-compose.yml for PostgreSQL:
docker-compose up -dThis is an open-source project. Contributions are welcome!
Check the repository for license information.
Yorgos S. (@smyrgeorge)
A comprehensive Kotlin multiplatform toolkit for building server applications with Ktor.
🏠 Homepage (under construction)
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!
implementation("io.github.smyrgeorge:ktkit:x.y.z")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.ElementThis 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.
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. |
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.
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.
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.
Authentication and authorization are built into the request pipeline of AbstractRestHandler.
Authentication — AbstractRestHandler 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-namemechanism 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 incomingx-real-nameheader, 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):
Principal carries a set of roles; the handler constructor accepts hasRole, hasAnyRole and
hasAllRoles constraints, enforced on every route of the handler.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") }) {
// ...
}
}
}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.
Every application serves two unauthenticated status endpoints out of the box:
GET /api/status/health — application name, status, start time, and uptimeGET /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)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 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.
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.
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.
@OpenApi metadata, configuration, limitations):
ktkit-compiler-openapi
Check the example application here.
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.shThen build as usual:
./gradlew buildThe project includes a docker-compose.yml for PostgreSQL:
docker-compose up -dThis is an open-source project. Contributions are welcome!
Check the repository for license information.
Yorgos S. (@smyrgeorge)