Skip to content

Latest commit

 

History

History

server

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

klite-server

This is the main component of Klite - the Server.

Create an instance, overriding any defaults using named constructor parameters, add contexts and routes, then start.

Basic usage:

  Server().apply {
    context("/api") {
      // Lambda routes
      get("/hello") { "Hello World" }
      post("/hello") { "You posted: $rawBody" }

      // or take routes from annotated functions of a class (better for unit tests)
      annotated<MyRoutes>()
    }

    start()
  }

See the sample subproject for a full working example.

Route handlers run in context of HttpExchange and can use its methods to work with request and response.

Anything returned from a handler will be passed to BodyRenderer to output the response with correct Content-Type. BodyRenderer is chosen based on the Accept request header or first one if no matches.

POST/PUT requests with body will be parsed using one of registered BodyParsers according to the request Content-Type header. The following body parsers are enabled by default:

use<JsonBody>() for application/json support.

Converter

Converter is used everywhere to convert incoming strings to the respective (value) types, e.g. request parameters, json fields, database values, etc.

This allows you to bind types like LocalDate or UUID directly in your routes, as well as Converter.use any custom types very easily, like Email, PhoneNumber, etc.

Contexts

All routes must be organized into contexts with path prefixes. A context with the longest matching path prefix is chosen for handling a request.

Assets (static content)

A simple AssetsHandler is provided to serve static files.

assets("/", AssetsHandler(Path.of("public")))

For SPA client-side routing support, create AssetsHandler with useIndexForUnknownPaths = true. Warning: this won't return 404 responses for missing paths anymore, but will render the index file.

Config

Config object is provided for an easy way to read System properties or env vars.

Use Config.fromEnvFile() if you want to load default config from an .env. This is useful for local development.

Registry (and Dependency Injection)

Registry and it's default implementation - DependencyInjectingRegistry - provide a simple way to register and require both Klite components and repositories/services of your application.

DependencyInjectingRegistry is used by default and can create any classes by recursively creating their constructor arguments (dependencies).

You can use register<MyInterface>(MyImplementation::class) or register<MyImplementation>() to register a specific implementation that needs to be used for an interface. Otherwise, calling require<MyClass>() will try to auto-create MyClass and all its dependencies, if any.

See it's tests for usage examples.

Decorators

You can add both global and context-specific decorators, including Before and After handlers. The order is important, and decorators apply to all following routes that are defined in the same context.

E.g. you can use the built-in CorsHandler.

Error handling

Any exception thrown out of route handler will be passed to ErrorHandler to produce a response. The ErrorResponse is then passed to BodyRenderer, like normal responses.

  Server().apply {
    errors.on<MyException>(BadRequest)
    errors.on<OtherException> {
      // some logic
      ErrorResponse(BadRequest, "custom message")
    }
  }

Sessions

Session support can be enabled by providing a SessionStore implementation, e.g.

  Server(sessionStore = CookieSessionStore())

The included CookieSessionStore stores sessions in an encrypted cookie, which doesn't require any synchronization between multiple server nodes. It requires a Config["SESSION_SECRET"] to be available to derive an encryption key. Make sure it is different in all your environments.

You can implement your own store if you want sessions to be stored in e.g. a database.

(SSE) Server-Sent Events

Supported using coroutines. Use exchange.startEventStream() and then exchange.send() in a loop. On the client-side, use browser's built-in EventSource class that will do reconnects automatically. See usage sample.

This is a much lighter alternative to WebSockets, based on HTTP, not a separate protocol.

HTML templates for server-side rendering

No built-in support for that. You may either implement a BodyRenderer that will pass route responses to your favorite template engine or just call the engine in your routes and produce html output directly with send(OK, "html", "text/html").

In Kotlin, you may also consider using template strings for html/xml generation, see the provided helpers:

get("/hello") {
  """<html><body><h1>Hello ${+query("who")}</h1></body></html>"""
}

The latter will be even better once string template processors become available in Kotlin.

Running behind a https proxy

In most production environments your app will be running behind a load balancer and https proxy. Proxies will forward some standard headers, that your app will need to understand:

  Server(requestIdGenerator = XRequestIdGenerator(), httpExchangeCreator = XForwardedHttpExchange::class.primaryConstructor!!)

Enable these only if you are sure that you will be running behind a trusted proxy in production.

Best practices

  • Organize your code into domain packages, e.g. payments, accounting, and not by type of class, e.g. controllers, repostories, services, etc.
  • Route handler's job is to parse the request, call a service method and transform/return the result. It should not implement business logic itself.
  • Prefer annotated route handlers for easier code separation and unit testing.
  • Do not catch common exceptions in your route handlers, but use ErrorHandler.on to add a global error handler instead based on Exception type, reducing code duplication.
  • Store only minimal state in a session, e.g. authenticated user id. Everything else should be part of the UI flow and support back/forward buttons.