Integrate with OpenAPI

This contract example shows:

  • 2 endpoints with typesafe contracts (marshalling of path parameters and bodies)
  • Custom filters (reporting latency)
  • API key security via a typesafe Query parameter (this can be a header or a body parameter as well)
  • A parameter lens that provides metadata for the output OpenApi schema node
  • OpenApi v3 documentation - Run this example and point a browser here

Gradle setup

dependencies {
    implementation(platform("org.http4k:http4k-bom:5.16.2.0"))
    implementation("org.http4k:http4k-core")
    implementation("org.http4k:http4k-contract")
    implementation("org.http4k:http4k-format-argo")
}

Note: although we use Argo here as our JSON API, you could also switch in any of the http4k-format-xxx JSON modules.

Code

package guide.howto.integrate_with_openapi

import org.http4k.contract.bind
import org.http4k.contract.contract
import org.http4k.contract.div
import org.http4k.contract.meta
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.ApiServer
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.contract.security.ApiKeySecurity
import org.http4k.core.Body
import org.http4k.core.ContentType.Companion.TEXT_PLAIN
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.HttpTransaction
import org.http4k.core.Method.GET
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Uri
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.CachingFilters.Response.NoCache
import org.http4k.filter.CorsPolicy
import org.http4k.filter.ResponseFilters
import org.http4k.filter.ServerFilters
import org.http4k.format.Argo
import org.http4k.format.Jackson
import org.http4k.lens.Path
import org.http4k.lens.Query
import org.http4k.lens.int
import org.http4k.lens.string
import org.http4k.routing.ResourceLoader.Companion.Classpath
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.http4k.routing.static
import org.http4k.server.Jetty
import org.http4k.server.asServer
import java.time.Clock

fun main() {

    fun add(value1: Int, value2: Int): HttpHandler = {
        Response(OK).with(
            Body.string(TEXT_PLAIN).toLens() of (value1 + value2).toString()
        )
    }

    val ageQuery = Query.int().required("age", "Your age", mapOf("schema" to mapOf("minimum" to 18)))
    fun echo(name: String): HttpHandler = {
        Response(OK).with(
            Body.string(TEXT_PLAIN).toLens() of "hello $name you are ${ageQuery(it)}"
        )
    }

    val filter: Filter =
        ResponseFilters.ReportHttpTransaction(Clock.systemUTC()) { tx: HttpTransaction ->
            println(tx.labels.toString() + " took " + tx.duration)
        }

    val mySecurity = ApiKeySecurity(Query.int().required("apiKey"), { it == 42 })

    val contract = contract {
        renderer = OpenApi3(
            ApiInfo("my great api", "v1.0"),
            Argo,
            servers = listOf(ApiServer(Uri.of("http://localhost:8000"), "the greatest server"))
        )
        descriptionPath = "/docs/openapi.json"
        security = mySecurity

        routes += "/ping" meta {
            summary = "add"
            description = "Adds 2 numbers together"
            returning(OK to "The result")
        } bindContract GET to { _ -> Response(OK).body("pong") }

        routes += "/add" / Path.int().of("value1") / Path.int().of("value2") meta {
            summary = "add"
            description = "Adds 2 numbers together"
            returning(OK to "The result")
        } bindContract GET to ::add

        // note here that the trailing parameter can be ignored - it would simply be the value "divide".
        routes += Path.int().of("value1") / Path.int().of("value2") / "divide" meta {
            summary = "divide"
            description = "Divides 2 numbers"
            returning(OK to "The result")
        } bindContract GET to { first, second, _ ->
            { Response(OK).body((first / second).toString()) }
        }

        routes += "/echo" / Path.of("name") meta {
            summary = "echo"
            queries += ageQuery
        } bindContract GET to ::echo
    }

    val handler = routes(
        "/context" bind filter.then(contract),
        "/static" bind NoCache().then(static(Classpath("guide/howto/nestable_routes"))),
        "/" bind contract {
            renderer = OpenApi3(ApiInfo("my great super api", "v1.0"), Jackson)
            routes += "/echo" / Path.of("name") meta {
                summary = "echo"
                queries += ageQuery
            } bindContract GET to ::echo
        }
    )

    ServerFilters.Cors(CorsPolicy.UnsafeGlobalPermissive).then(handler).asServer(Jetty(8000))
        .start()
}

// Ping!                    curl -v "http://localhost:8000/context/ping?apiKey=42"
// Adding 2 numbers:        curl -v "http://localhost:8000/context/add/123/564?apiKey=42"
// Echo (fail):             curl -v "http://localhost:8000/context/echo/myName?age=notANumber&apiKey=42"
// API Key enforcement:     curl -v "http://localhost:8000/context/add/123/564?apiKey=444"
// Static content:          curl -v "http://localhost:8000/static/someStaticFile.txt"
// OpenApi/Swagger documentation:   curl -v "http://localhost:8000/context/docs/openapi.json"
// Echo endpoint (at root): curl -v "http://localhost:8000/echo/hello?age=123"