Server Applications

Ktor server introduction and key concepts

A running instance of a ktor application is represented byApplication class.A ktor application consists of a set of (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 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 for more details.

Features

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 {}.

In ktor a pair of incoming request and response (complete or incomplete)is named ApplicationCall.Every application call is passed through an consisting 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. }

A pipeline may consist of several phases. Every interceptor is registered at a particular phase.So interceptors are executed in their phases order. See 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).

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

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:

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. call.respondText("Hello, World!")
  3. }
  4. get("/profile/{id}") { TODO("...") }
  5. }

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

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

Example:

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