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:
text/plain
- TextBodyParserapplication/x-www-form-urlencoded
- FormUrlEncodedParsermultipart/form-data
- FormDataParser
use<JsonBody>() for application/json
support.
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.
All routes must be organized into contexts with path prefixes. A context with the longest matching path prefix is chosen for handling a request.
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 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 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.
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.
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")
}
}
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.
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.
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.
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:
- To support
X-Request-Id
header (e.g. in Heroku), pass XRequestIdGenerator instance to the Server - To support
X-Forwarded-For
and the like, pass XForwardedHttpExchange constructor to the Server
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.
- 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.