Layouting
Layouts are a basic mechanism that allows you to assemble your web application from different parts. They define which URLs are made available by the server and what the URL structure of your app will look like.
The following example will expose a single file to your clients which will be available
via the URL http://localhost:8080/hello.txt. For any other URL, the server will generate
a 404 Not Found response.
val app = Layout.create()
.add("hello.txt", Content.from(Resource.fromString("Hello World")))
Host.create()
.handler(app)
.run()In this example, we create a segment named hello.txt and let it be handled by the Content handler.
Sub Sections
Layouts can contain another layout for a specific path. This way your web application can be structured as needed. The following example exposes http://localhost:8080/static/hello.txt to your clients.
val resources = Layout.create()
.add("hello.txt", Content.from(Resource.fromString("Hello World")))
val app = Layout.create()
.add("static", resources)
Host.create()
.handler(app)
.run()This feature is typically used to describe your project topology on a high level and to glue everything together. The following example will give you an idea on how this could be used in a more complex app.
val api = Inline.create()
.get { "Hello World" }
val static = Assets.from("./static")
val adminArea = Layout.create()
val app = Layout.create()
.add("api", api)
.add("static", static)
.add("admin-area", adminArea)
Host.create()
.handler(app)
.run()Multiple Segments
To create a nested path structure (such as /api/v1/) you can nest layouts within layouts.
To make this easier, there is an extension function that accepts an array of segments and internally creates additional layouts as needed:
class BookService {
@ResourceMethod
fun getBooks(): List<String> = listOf("Lord of the Rings")
}
val api = Layout.create()
.addService<BookService>("books")
val app = Layout.create()
.add(arrayOf("api", "v1"), api)
Host.create()
.handler(app)
.run()Alternatively, you can also write .add("/api/v1/", api) which is less type safe yet easier to understand.
Imperative Flow
Instead of creating and adding new layouts for each sub section yourself, you can also directly create a new layout
by calling the addSegment() function on an existing builder. This flavor feels more imperative and might suit some project
setup procedures better than the functional one.
class SomeService {
@ResourceMethod
fun hello(): String = "Hello World"
}
val app = Layout.create()
val api = app.addSegment("api") // or addSegments("api", "v1")
api.addService<SomeService>("some")Fallbacks
When adding handlers without a segment name, they will be called if no other registered handler was able
to generate a response. In the following example, http://localhost:8080/file/
will return File Content whereas any other URL will return Fallback Content.
val app = Layout.create()
.add("file", Content.from(Resource.fromString("File Content")))
.add(Content.from(Resource.fromString("Fallback Content")))
Host.create()
.handler(app)
.run()If there are multiple routes set on a layout, the implementation will invoke them one-by-one until it retrieves a non-null response. In the following example, the server will render the “Hello World” response only, if the requested file does not exist in the given directory.
val tree = ResourceTree.fromDirectory("/var/www/files")
val files = Assets.from(tree)
val app = Layout.create()
.add(files)
.add(Content.from(Resource.fromString("Hello World")))
Host.create()
.handler(app)
.run()Index
While the add() function matches all routes that start with the given name (e.g. /hello.txt/appendix
in the first example of this document), the index() function only matches the current root. The following example
will render Hello World when http://localhost:8080/ is requested, but returns
404 Not Found for any other URL.
val app = Layout.create()
.index(Content.from(Resource.fromString("Hello World")))
Host.create()
.handler(app)
.run()Extensions
Some modules extend the layout builder to reduce the boilerplate code required to add handlers or concerns to your application.
For example the webservice module allows to directly add a webservice handler.
class MyServiceClass {
@ResourceMethod
fun hello(): String = "Hello World"
}
val api = Layout.create()
.addService<MyServiceClass>("service")which is a shortcut for
class MyServiceClass {
@ResourceMethod
fun hello(): String = "Hello World"
}
val service = ServiceResource.from<MyServiceClass>()
val api = Layout.create()
.add("service", service)As those functions are extension functions provided by the source module,
this requires you to import them, in this case with import org.genhttp.modules.webservices.addService
(or import org.genhttp.modules.webservices.*).
Non-Root Layouts
While it makes sense to start with a layout on root level to define the structure of your application, it is just an ordinary handler and your server instance just requires any handler to work with.
This means that you do not need to use a layout as a root handler and can add layouts anywhere where handlers are supported.
The following example uses the virtual host handler to serve two different kinds of apps to your clients:
val app1 = Layout.create()
val app2 = Layout.create()
val app = VirtualHosts.create()
.add("domain1.com", app1)
.add("domain2.com", app2)
Host.create()
.handler(app)
.run()If you are inspecting the handler chain at runtime (e.g. to build a custom OpenAPI explorer), note
that a layout is built into a LayoutHandler instance.