Iwf Guide

Iwf Guide

JSON API and OpenAPI

Iwf JSON routes use the same typed route metadata as HTML routes. The goal is one source of truth for request decoding, response typing, links, redirects, form compatibility, and OpenAPI.

OpenAPI generation lives in the separate openapi package. Add openapi to the application package depends and import Iwf.OpenApi where you build docs routes or print the generated document.

1. JSON Routes

Use one controller handler shape for HTML and JSON. Path and query inputs come from the route string; the body comes from the second handler argument; response metadata comes from the handler result type.

record CommentParams where
  constructor MkCommentParams
  slug : String
  notify : Maybe Bool

record CommentBody where
  constructor MkCommentBody
  body : String

record CommentEnvelope where
  constructor MkCommentEnvelope
  commentId : Integer
  body : String

%runElab deriveDto "CommentBody"
%runElab deriveDto "CommentEnvelope"

createCommentApi : CommentParams -> CommentBody -> ControllerContext -> CommentEnvelope
createCommentApi params body context =
  createComment params.slug params.notify body.body

createCommentRoute : RouteSpec CommentParams CommentBody CommentEnvelope
createCommentRoute =
  post "/api/articles/{slug}/comments?{notify}"
    |> params CommentParams
    |> body CommentBody
    |> respondsJson CommentEnvelope
    |> describe "Create comment"

createCommentBinding : BoundRoute
createCommentBinding =
  handledBy createCommentRoute createCommentApi

deriveDto is the canonical helper for records that are shared between JSON decoding, JSON encoding, form decoding, and OpenAPI schema generation. It derives Show, Eq, ToJSON, FromJSON, ToSchema, and FormFields, which also creates the generated form field values used by typed forms and request-body decoding.

The framework decodes path params, query params, and the request body before the handler runs. JSON and urlencoded form bodies share the same handler shape; the server picks the decoder from Content-Type. The route spec records the JSON response type, and handledBy checks that the handler returns that type.

2. Content Negotiation

Use respondNegotiated inside a normal context handler when a route supports both HTML and JSON:

record ArticleInput where
  constructor MkArticleInput
  slug : String

articleShow : ArticleInput -> ControllerContext -> Response
articleShow input context =
  respondNegotiated
    context.request
    (articleHtml input.slug)
    (articlePayload input.slug)

articleRoute : RouteSpec ArticleInput NoInput Response
articleRoute =
  get "/article/{slug}" |> params ArticleInput |> html

articleBinding : BoundRoute
articleBinding =
  handledBy articleRoute articleShow

The route returns HTML by default and JSON when the request Accept header prefers application/json. HTMX headers do not force a JSON request into an HTML fragment.

Use respondsJson when a route returns JSON data, or leave the response type as Response when the handler owns content negotiation directly.

3. OpenAPI Info

Describe the API:

import Iwf.OpenApi

apiInfo : OpenApiInfo
apiInfo =
  MkOpenApiInfo
    "Conduit API"
    "1.0.0"
    (Just "RealWorld-style Iwf API")

Mount Swagger UI:

routesWithDocs : Routes
routesWithDocs =
  appRoutes ++ swaggerUiRoutes "/docs" apiInfo appRoutes

Add generation options when the document needs deployment metadata, security metadata, tags, external docs, or schema overrides:

apiOptions : OpenApiOptions
apiOptions =
  MkOpenApiOptions
    [MkOpenApiServer "https://api.example.com" (Just "Production")]
    [MkOpenApiSecurityScheme "bearerAuth" bearerSecurityScheme]
    [MkOpenApiSecurityRequirement "bearerAuth" []]
    [MkOpenApiTag "Articles" (Just "Article endpoints")]
    (Just (MkOpenApiExternalDocs (Just "API guide") "https://docs.example.com/api"))
    []

docs : OpenJson
docs =
  openApiDocumentWithOptions apiInfo apiOptions appRoutes

routesWithOptions : Routes
routesWithOptions =
  appRoutes ++ swaggerUiRoutesWithOptions "/docs" apiInfo apiOptions appRoutes

Open:

http://127.0.0.1:8082/docs
http://127.0.0.1:8082/docs/openapi.json

The canonical app exposes its generated document at:

http://127.0.0.1:8082/docs/openapi.json

4. Swagger UI Customization

swaggerUiRoutes is the standard mount for local API docs. It creates two undocumented routes: the HTML shell at the path you pass in and the generated document at <path>/openapi.json.

Use swaggerUiHtml when the app wants to own the route shape but keep Iwf's default Swagger UI shell:

customDocsRoutes : Routes
customDocsRoutes =
  [ undocumentedRoute [GET, HEAD] "/internal/api-docs"
      (\_ => okHtml (swaggerUiHtml apiInfo "/internal/openapi.json"))
  , undocumentedRoute [GET, HEAD] "/internal/openapi.json"
      (\_ => okJson (show (openApiDocumentWithOptions apiInfo apiOptions appRoutes)))
  ]

For deeper customization, mount an app-owned HTML route and point Swagger UI at show (openApiDocumentWithOptions apiInfo apiOptions appRoutes). The built-in shell uses the Swagger UI CDN and a fixed SwaggerUIBundle setup; apps that need different assets or initialization code should own the HTML route and reuse the generated JSON document.

5. OpenAPI Options

OpenApiOptions covers document-level metadata:

  • servers
  • securitySchemes
  • security
  • tags
  • externalDocs
  • componentSchemaOverrides

Use componentSchemaOverrides when a generated schema needs a hand-authored OpenAPI shape. Overrides are keyed by component schema name and replace the generated schema for that name. Extra override names are added to components.schemas.

6. Response Schema Checks

Use validateJsonTextAgainstSchema in tests for JSON endpoints that also publish OpenAPI schemas:

response : Response
response =
  runRoutes appRoutes (testGet "/api/articles/1")

schemaCheck : Either String ()
schemaCheck =
  validateJsonTextAgainstSchema articlePayload response.body

The validator checks required fields, optional null, arrays, nested objects, scalar JSON kinds, and enum values against SchemaDoc.

7. What Is Documented

Documented route metadata includes:

  • path params
  • query params
  • request body schemas
  • response body schemas
  • supported methods
  • summaries
  • scalar formats for dates, datetimes, UUIDs, IDs, enums, arrays, and optional values

Only documented routes appear in OpenAPI. Use undocumentedRoute for internal routes such as health checks, framework assets, and private operational endpoints.

8. RealWorld Shape

The canonical app exposes browser pages and API routes for:

  • login and registration
  • current user
  • profiles
  • follow/unfollow
  • article CRUD
  • comments
  • favorites
  • feeds
  • tags

Those routes share types and validation with the HTML side where practical.

Next

Read Sessions, Cookies, Flash, and CSRF.