Basic: Nestable routes

This is a fairly comprehensive example of the core-routing logic available:

  • Individual HTTP endpoints are represented as HttpHandlers.
  • Binding an HttpHandler to a path and HTTP verb yields a Route.
  • Routes can be combined together into a RoutingHttpHandler, which is both an HttpHandler and aRouter.
  • A Router is a selective request handler, which attempts to match a request. If it cannot, processing falls through to the next Router in the list.
  • Routers can be combined together to form another HttpHandler
  • Usage of supplied core library Filters
  • Serving of static content using a Classpath resource loader
  • Support for Single Page Applications using a singlePageApp() block - resources loaded from here are loaded from the underlying ResourceLoader or fallback to / (and passed to the SPA code)

Dynamic Paths / Path Variables

As you would expect, http4k allows routes to include dynamic or variable elements in the matching path, and allows you to reference the variable in the Handler. For example:

"/book/{title}" bind GET to { req -> 
    Response.invoke(Status.OK).body(GetBookDetails(req.path("title")) 
}
"/author/{name}/latest" bind GET to { req -> 
    Response.invoke(Status.OK).body(GetAllBooks(author = req.path("name")).last()) 
}

By default, the variable(s) will match anything. However you can append the variable name with a RegEx expression to limit the matches.

// will match /book/978-3-16-148410-0 (i.e. only digits and dashes)
// /book/A%20Confederacy%20Of%20Dunces would return a 404 (Not Found)
"/book/{isbn:[\\d-]+}"

// will NOT match /sales/south or /sales/usa
"/sales/{region:(?:northeast|southeast|west|international)}" 

There are no pre-defined types such as int or path for matching but these are easy to replicate with RegEx's: - string (excluding slashes) : [^\\/]+ (note that Kotlin requires backslashes to be escaped, so \w in RegEx is expressed as \\w in Kotlin) - int : \\d+ - float : [+-]?([0-9]*[.])?[0-9]+ (this will match basic floats. Does not match exponents, or scientific notation) - path : .*

Note that paths, not strings, will match by default. "/news/{date}" will match www.example.com/news/2018/05/26, making request.path("date") equal to 2018/05/26. This may be exactly what you want, or it may produce unexpected results, depending on how your URLs are structured.

Gradle setup

dependencies {
    implementation(platform("org.http4k:http4k-bom:5.14.4.0"))
    implementation("org.http4k:http4k-core")
}

Code

package guide.howto.nestable_routes

import org.http4k.core.Method.DELETE
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.DebuggingFilters.PrintRequestAndResponse
import org.http4k.routing.ResourceLoader.Companion.Classpath
import org.http4k.routing.and
import org.http4k.routing.bind
import org.http4k.routing.header
import org.http4k.routing.headers
import org.http4k.routing.path
import org.http4k.routing.queries
import org.http4k.routing.routes
import org.http4k.routing.singlePageApp
import org.http4k.routing.static

fun main() {
    val routesWithFilter =
        PrintRequestAndResponse().then(
            routes(
                "/get/{name}" bind GET to { req: Request ->
                    Response(OK).body(req.path("name")!!)
                },
                "/post/{name}" bind POST to { Response(OK) }
            )
        )
    println(routesWithFilter(Request(GET, "/get/value")))

    val staticWithFilter = PrintRequestAndResponse()
        .then(static(Classpath("guide/howto/nestable_routes")))

    val app = routes(
        "/bob" bind routesWithFilter,
        "/static" bind staticWithFilter,
        "/pattern/{rest:.*}" bind { req: Request ->
            Response(OK).body(req.path("rest").orEmpty())
        },
        "/rita" bind routes(
            "/delete/{name}" bind DELETE to { Response(OK) },
            "/post/{name}" bind POST to { Response(OK) }
        ),
        "/matching" bind GET to routes(
            header("requiredheader", "somevalue")
                .and(queries("requiredquery")) bind {
                Response(OK).body("matched 2 parameters")
            },
            headers("requiredheader") bind { Response(OK).body("matched 1 parameters") }
        ),
        singlePageApp(Classpath("guide/howto/nestable_routes"))
    )

    println(app(Request(GET, "/bob/get/value")))
    println(app(Request(GET, "/static/someStaticFile.txt")))
    println(app(Request(GET, "/pattern/some/entire/pattern/we/want/to/capture")))
    println(
        app(
            Request(GET, "/matching")
                .header("requiredheader", "somevalue")
                .query("requiredquery", "somevalue")
        )
    )
    println(app(Request(GET, "/matching").header("requiredheader", "somevalue")))
    println(app(Request(GET, "/someSpaResource")))
}

For the typesafe contract-style routing, refer to this recipe instead,