4

I'm building a microservice with the http4k framework using their Contract APIs. I can easily expose the swagger API description JSON on eg. /swagger.json with

fun app(): HttpHandler = "/" bind contract {
    renderer = OpenApi3(ApiInfo("GoOut Locations API", "1.0"), Jackson)
    descriptionPath = "/swagger.json"
    routes += ...
}

Is there an easy way to expose the swagger UI so that 1) I can specify the path it will be available on. (eg. /swagger-ui) 2) The UI will be preconfigured to fetch the description JSON from the descriptionPath specified above.

An ideal API would look something like

fun app(): HttpHandler = "/" bind contract {
    renderer = OpenApi3(ApiInfo("GoOut Locations API", "1.0"), Jackson)
    descriptionPath = "/swagger.json"
    uiPath = "/swagger-ui"
    routes += ...
}
Jen
  • 1,206
  • 9
  • 30

4 Answers4

3

After a bit of searching I achieved this with combination of Web Jars and http4k's static routing.

The potential viewer of the docs must simply visit /docs path where he gets redirected to /docs/index.html?url=<path to Api description> where

  • index.html is a static Swagger UI entrypoint served from a web jar.
  • url query param tells the swagger UI where to fetch the OpenApi description from.

From the DX perspective we have a simple http4k application:

// path the OpenApi description will be exposed on
private const val API_DESCRIPTION_PATH = "/swagger.json"

fun app(): HttpHandler {
    val api = contract {
        renderer = OpenApi3(ApiInfo("Your API summary", "1.0"), Jackson)
        descriptionPath = API_DESCRIPTION_PATH
        // the actual API routes
        routes += ... 
    }

     return routes(
         // the docs routes are not considered part of the API so we define them outside of the contract
         swaggerUi(API_DESCRIPTION_PATH),
         api
     )
}

The swaggerUi handler implementation follows

/**
 * Exposes Swagger UI with /docs path as its entry point.
 * @param descriptionPath absolute path to API description JSON. The UI will be configured to fetch it after load.
 */
fun swaggerUi(descriptionPath: String): RoutingHttpHandler = routes(
    "docs" bind Method.GET to {
        Response(Status.FOUND).header("Location", "/docs/index.html?url=$descriptionPath")
    },
    // For some reason the static handler does not work without "/" path prefix.
    "/docs" bind static(Classpath("META-INF/resources/webjars/swagger-ui/3.25.2"))
)

We also have to include the swagger-ui webjar as our dependency. Here's a Gradle directive:

implementation 'org.webjars:swagger-ui:3.25.2'

See the webjars website for Maven (and more) directives.

Note that the swaggerUi handler assumes its bound to the / root path of the whole service. However, that can be easily fixed.

Jen
  • 1,206
  • 9
  • 30
2

The solution using the webjar does not work anymore for SwaggerUI version >= 4.1.3 as the url parameter is ignored (see this issue / the release notes). The URL has to be either specified in the HTML or the url parameter needs to be enabled in the HTML. So for now the solution seems to be to unpack the UI, update index.html, and serve directly rather through the webjar.

Dhs3000
  • 21
  • 3
2

As of http4k 4.28.1.0, there is now a way to do this. See the following code taken from this documentation page:

package guide.howto.create_a_swagger_ui

import org.http4k.contract.contract
import org.http4k.contract.meta
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.contract.ui.swaggerUi
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Uri
import org.http4k.core.with
import org.http4k.lens.string
import org.http4k.routing.routes
import org.http4k.server.SunHttp
import org.http4k.server.asServer

fun main() {
    val greetingLens = Body.string(ContentType.TEXT_PLAIN).toLens()

    // Define a single http route for our contract
    val helloHandler = "/v1/hello" meta {
        operationId = "v1Hello"
        summary = "Say Hello"
        returning(OK, greetingLens to "Sample Greeting")
    } bindContract GET to { _: Request ->
        Response(OK).with(greetingLens of "HI!")
    }

    // Define a contract, and render an OpenApi 3 spec at "/spec"
    val v1Api = contract {
        routes += helloHandler
        renderer = OpenApi3(
            ApiInfo("Hello Server - Developer UI", "99.3.4")
        )
        descriptionPath = "spec"
    }

    // Build a Swagger UI based on the OpenApi spec defined at "/spec"
    val ui = swaggerUi(
        Uri.of("spec"),
        title = "Hello Server",
        displayOperationId = true
    )

    // Combine our api, spec, and ui; then start a server
    // The Swagger UI is available on the root "/" path
    routes(v1Api, ui)
        .asServer(SunHttp(8080))
        .start()
        .block()
}
Jonathan Hult
  • 1,351
  • 2
  • 13
  • 19
1

http4k doesn't ship with a version of the OpenApi UI. You can easily ship a version of the UI though by:

  1. unpacking the OpenApi UI into the src/main/resources/public folder
  2. Using a static routing block to server the resources. There is an example of that here: https://github.com/http4k/http4k-by-example/blob/22dcc9a83c497253c29830d5bc981afa5fbbe4ff/src/main/kotlin/verysecuresystems/SecuritySystem.kt#L61
DaveD
  • 71
  • 3
  • Thanks a lot for your answer! In the meantime I already solved this and posted the solution below. Since my answer is a bit more complete I plan to mark it as the accepted answer. (once the two dat limit passes) Hope, that's ok for you. .) – Jen May 13 '20 at 11:11