Server Applications

Ktor server introduction and key concepts

Table of contents:

Application and ApplicationEnvironment

A running instance of a ktor application is represented byApplication class.A ktor application consists of a set of modules (possibly one).Each module is a regular kotlin lambda or a function(usually having an instance of application as a receiver or parameter).

An application is started inside of an environment that is represented byApplicationEnvironmenthaving an application config (See Configuration page for more details).

A ktor server is started with an environment and controls the application lifecycle. An application instance is createdand destroyed by the environment (depending on the implementation it could create it lazilyor provide hot reload functionality).So stopping the application doesn’t always mean that the server is stopping: for example, it could be reloaded while the server keeps running.

Application modules are started one by one when an application is started, and every module can configure an instanceof the application. An application instance is configured by installing features and intercepting pipelines.

See lifecycle for more details.

Features

A feature is a piece of specific functionality that could be plugged into an application. It usually _intercepts_requests and responses and does its particular functionality.For example, the Default Headers feature intercepts responsesand appends Date and Server headers. A feature can be installed into an application using the install functionlike this:

  1. application.install(DefaultHeaders) {
  2. // configure feature
  3. }

For some features, the configuration lambda is optional. In this case, the feature can only be installed once. However,there are cases when a configuration composition is required. For such features, there are helper functionsthat install a feature if it is not yet installed and apply a configuration lambda. For example, routing {}.

Calls and pipelines

In ktor a pair of incoming request and response (complete or incomplete)is named ApplicationCall.Every application call is passed through an ApplicationCallPipelineconsisting of several (or none) interceptors. Interceptors are invoked one by one and every _interceptor_can amend the request or response and control pipeline execution by proceeding (proceed()) to the next interceptoror finishing (finish() or finishAll()) the whole pipeline execution(so the next interceptor is not invoked,see PipelineContext for details).It can also decorate the remaining interceptors chain doing additional actions before and after proceed() invocation.

Consider the following decorating example:

  1. intercept {
  2. myPrepare()
  3. try {
  4. proceed()
  5. } finally {
  6. myComplete()
  7. }
  8. }

A pipeline may consist of several phases. Every interceptor is registered at a particular phase.So interceptors are executed in their phases order. See Pipelines documentationfor a more detailed explanation.

Application call

An application call consists of a pair of request with response and a set of parameters.So an application call pipeline has a pair of receive and send pipelines. The request’s content (body) could be received using ApplicationCall.receive<T>() where T is an expected type of content. For example, call.receive<String>() reads the request body as a String. Some types could be received with no additional configuration out of the box, while receiving a custom type may require a feature installation or configuration. Every receive() causes the receive pipeline (ApplicationCallPipeline.receivePipeline) to be executed from the beginning so every receive pipeline interceptor could transform or by-pass the request body. The original body object type is ByteReadChannel (asynchronous byte channel).

An application response body could be provided by ApplicationCall.respond(Any) function invocation thatexecutes a response pipeline (ApplicationCallPipeline.respondPipeline). Similar to receive pipeline,every response pipeline interceptor could transform the response object. Finally, a response object should beconverted into an instance ofOutgoingContent.

A set of extension functions respondText, respondBytes, receiveText, receiveParameters and so on simplify the construction of request and response objects.

Routing

An empty application has no interceptors so 404 Not Found will be generated for every request. An application call pipeline should be intercepted to handle requests. An interceptor can respond depending onthe request URI like this:

  1. intercept {
  2. val uri = call.request.uri
  3. when {
  4. uri == "/" -> call.respondText("Hello, World!")
  5. uri.startsWith("/profile/") -> { TODO("...") }
  6. }
  7. }

For sure, this approach has a lot of disadvantages.Fortunately, there is the Routing feature for structured requesthandling that intercepts the application call pipeline and provides a way to register handlers for routes.Since the only thing Routing does is intercept the application call pipeline, manual interception with Routing also works.Routing consists of a tree of routes having handlers and interceptors. A set of extension functions in ktorprovides an easy way to register handlers like this:

  1. routing {
  2. get("/") {
  3. call.respondText("Hello, World!")
  4. }
  5. get("/profile/{id}") { TODO("...") }
  6. }

Notice that routes are organized into a tree so you can declare structured routes:

  1. routing {
  2. route("profile/{id}") {
  3. get("view") { TODO("...") }
  4. get("settings") { TODO("...") }
  5. }
  6. }

A routing path can contain constant parts and parameters such as {id} in the example above.The property call.parameters provides access to the captured setting values.

Content Negotiation

ContentNegotiation provides a way to negotiatemime types and convert types using Accept and Content-Type headers.A content converter can be registered for a particular content type for receiving and responding objects.There are Jackson, Gson and kotlinx.serialization content converters available out of the box that can be plugged into the feature.

Example:

  1. install(ContentNegotiation) {
  2. gson {
  3. // Configure Gson here
  4. }
  5. }
  6. routing {
  7. get("/") {
  8. call.respond(MyData("Hello, World!"))
  9. }
  10. }

What’s next