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.

Kotlin
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.

Kotlin
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.

Kotlin
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:

Kotlin
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.

Kotlin
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.

Kotlin
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.

Kotlin
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.

Kotlin
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.

Kotlin
class MyServiceClass {

    @ResourceMethod
    fun hello(): String = "Hello World"

}

val api = Layout.create()
    .addService<MyServiceClass>("service")

which is a shortcut for

Kotlin
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:

Kotlin
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.