Integrate with OpenAPI
http4k Core
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:6.0.0.0"))
implementation("org.http4k:http4k-core")
implementation("org.http4k:http4k-api-openapi")
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 content.howto.integrate_with_openapi
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.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.CacheResponse.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"