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:
serverssecuritySchemessecuritytagsexternalDocscomponentSchemaOverrides
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.