REST
Kairo is built on top of Ktor, so it supports all the features of Ktor.
Kairo also provides an advanced routing DSL, which is optional but highly recommended.
Installation
Section titled “Installation”Install kairo-rest-feature.
You don’t need to install Ktor separately —
it’s included by default.
dependencies { implementation("software.airborne.kairo:kairo-rest-feature")}First, add the Feature to your Server.
val features = listOf( RestFeature( config = config.rest, authConfig = null, // This Server won't support auth. See below for an auth example. ),)We recommend using kairo-config to configure the Feature.
rest { connector.port = 8080 plugins { defaultHeaders.serverName = "..." }}The DSL
Section titled “The DSL”Define your endpoints
Section titled “Define your endpoints”First, define your endpoints in a type-safe way.
data class UserRep( val id: UserId, val createdAt: Instant, val emailAddress: String,) { data class Creator( val emailAddress: String, )}
object UserApi { @Rest("GET", "/users/:userId") @Rest.Accept("application/json") data class Get( @PathParam val userId: UserId, ) : RestEndpoint<Unit, UserRep>()
@Rest("GET", "/users") @Rest.Accept("application/json") data class GetByEmailAddress( @QueryParam val emailAddress: String, ) : RestEndpoint<Unit, UserRep>()
@Rest("GET", "/users") @Rest.Accept("application/json") data object ListAll : RestEndpoint<Unit, List<UserRep>>()
@Rest("POST", "/users") @Rest.ContentType("application/json") @Rest.Accept("application/json") data class Create( override val body: UserRep.Creator, ) : RestEndpoint<UserRep.Creator, UserRep>()}Implement your handlers
Section titled “Implement your handlers”@Singleclass UserHandler( private val userMapper: UserMapper, private val userService: UserService,) : HasRouting { override fun Application.routing() { @Suppress("LongMethod") routing { route(UserApi.Get::class) { handle { val user = userService.get(endpoint.userId) ?: throw UserNotFound(endpoint.userId) userMapper.rep(user) } }
route(UserApi.GetByEmailAddress::class) { handle { val user = userService.getByEmailAddress(endpoint.emailAddress) ?: throw UserNotFound(null) userMapper.rep(user) } }
route(UserApi.ListAll::class) { handle { val users = userService.listAll() users.map { userMapper.rep(it) } } }
route(UserApi.Create::class) { handle { val user = userService.create( creator = userMapper.creator(endpoint.body), ) userMapper.rep(user) } } } }}If you want your Server to support auth, you must provide an AuthConfig.
class MyAuth : AuthConfig() { /** * Your implementation can be anything; * this shows you how to use JWK for JWT. */ override fun AuthenticationConfig.configure() { val jwkProvider = JwkProviderBuilder(URI.create(config.jwkUrl).toURL()).build() jwt { credential -> val decoded = JWT.decode(credential.token) val jwk = jwkProvider.get(decoded.keyId ?: throw JwtVerificationFailed()) val algorithm = Algorithm.RSA256(jwk.publicKey as RSAPublicKey, null) return@jwt JWT .require(algorithm) .withIssuer(config.issuer) .acceptLeeway(config.leeway.inWholeSeconds) .build() } }}
RestFeature( config = config.rest, authConfig = MyAuth(),)Advanced configuration
Section titled “Advanced configuration”This section documents all available configuration options.
Parallelism
Section titled “Parallelism”The Parallelism config specifies how to manage thread pools.
runningLimit: The maximum number of concurrent requests. Defaults to 50.shareWorkGroup: Whether to avoid creating a call group, sharing the worker group instead. Defaults to false.connectionGroupSize: How many threads for accepting new connections and starting call processing. Defaults to 1. If explicitly null, uses Netty’s default.workerGroupSize: How many threads for processing connections, parsing messages and doing Netty’s internal work. Defaults to 1. If explicitly null, uses Netty’s default.callGroupSize: How many threads for processing application calls. Defaults to 10. If explicitly null, uses Netty’s default.
Timeouts
Section titled “Timeouts”The Timeouts config specifies how timeouts work.
requestRead: Defaults to 0, which is infinite.responseWrite: Defaults to 10 seconds.
Lifecycle
Section titled “Lifecycle”The Lifecycle config specifies Server lifecycle.
shutdownGracePeriod: The minimum amount of time to wait for cooldown. Defaults to 0.shutdownTimeout: The maximum amount of time to wait for cooldown. Defaults to 15 seconds.
Connector
Section titled “Connector”The Connector config specifies REST connectivity.
host: Which host to listen on. Defaults to unmasked.port: The port to listen on. No default (required).
Plugins.AutoHeadResponse
Section titled “Plugins.AutoHeadResponse”Configures Ktor’s AutoHeadResponse plugin.
Defaults to null, which means the plugin is disabled.
Plugins.CallLogging
Section titled “Plugins.CallLogging”Configures Ktor’s CallLogging plugin.
Enabled by default. Disable by setting to null.
useColors: Whether to use colors in the logs. Defaults to false.
Plugins.ContentNegotiation
Section titled “Plugins.ContentNegotiation”Configures Ktor’s ContentNegotiation plugin.
Enabled by default. Disable by setting to null.
Plugins.Cors
Section titled “Plugins.Cors”Configures Ktor’s Cors plugin.
Defaults to null, which means the plugin is disabled.
hosts: An array of allowed hosts.hosts.hostThe host to allow.hosts.schemes: The schemes to allow.hosts.subdomains: The subdomains to allow.headers: The headers to allow.methods: The methods to allow.allowCredentials: Whether to allow credentials.
Plugins.DefaultHeaders
Section titled “Plugins.DefaultHeaders”Configures Ktor’s DefaultHeaders plugin.
Enabled by default. Disable by setting to null.
serverName: The server name for theServerheader. No default (required).headers: A map of default headers.
Plugins.DoubleReceive
Section titled “Plugins.DoubleReceive”Configures Ktor’s DoubleReceive plugin.
Defaults to null, which means the plugin is disabled.
cacheRawRequest: Whether to cache the raw request.
Plugins.ForwardedHeaders
Section titled “Plugins.ForwardedHeaders”Configures Ktor’s ForwardedHeaders plugin.
Enabled by default. Disable by setting to null.
Plugins.Sse
Section titled “Plugins.Sse”Configures Ktor’s Sse plugin.
Defaults to null, which means the plugin is disabled.
Exception handling
Section titled “Exception handling”If you use Kairo’s logical errors, they will automatically be mapped to semantic HTTP responses.
data class UserNotFound( val userId: UserId?,) : LogicalFailure("User not found") { override val type: String = "UserNotFound" override val status: HttpStatusCode = HttpStatusCode.NotFound
override fun Map<String, Any?>.buildJson() { put("userId", userId) }}
// => {// "type": "UserNotFound",// "status": 404,// "message": "User not found",// "detail": null,// "userId": "..."// }Deserialization errors
Section titled “Deserialization errors”Deserialization errors will be mapped to semantic HTTP responses.
data class Rep(val value: Int)
// POST {}// -> {// "type": "MissingProperty",// "status": 400,// "message": "Missing property",// "detail": null,// "path": "/value"// }
// POST {"value":"Hello, World!"}// -> {// "type": "InvalidProperty",// "status": 400,// "message": "Invalid property",// "detail": null,// "path": "/value"// }
// POST {"value":42,"other":"Hello, World!"}// -> {// "type": "UnrecognizedProperty",// "status": 400,// "message": "Unrecognized property",// "detail": null,// "path": "/other"// }Logging config
Section titled “Logging config”We recommend excluding logs below the INFO level for this library.
<Logger name="io.ktor.server.plugins.cors.CORS" level="warn"/><Logger name="kairo.rest" level="info"/>