---
title: Serving Documentation
description: Export an HTTP contract, serve cached JSON, and mount a bundled Swagger UI with explicit access controls.
order: 30
summary:
  - Export deterministic JSON as an artifact or serve a document cached on its first request.
  - Mount bundled Swagger UI assets and choose safe base paths and document URLs.
  - Protect the JSON contract and complete viewer subtree with explicit access and browser policies.
---

# Serving Documentation

You can distribute your HTTP contract as a build artifact without exposing a
documentation endpoint. If clients need live access, register a JSON handler;
if people need a browser viewer, add Swagger UI separately. None of these choices
starts a listener or installs authentication.

The examples assume an annotated `routes` router, as introduced in the
[overview](overview.md). Finish registration before exporting concurrently or
serving requests.

## Export JSON

For a contract test, client generator, or release artifact, export after registering
the API routes:

```go
document, err := routes.OpenAPI("Greeting API", "1.0.0")
```

On success, `document` holds OpenAPI **3.2.1** JSON bytes. Save them through your
build's artifact-writing step. Identical declarations and document settings
produce identical bytes; no application startup, listener, or request is needed.

Title and API version must be nonempty. Invalid declarations, duplicate operation
IDs, and equivalent path templates return contextual errors instead of a partial
document. Make export success a test or build requirement before publishing.
This checks [declarations](operations.md#check-declarations), not whether handlers
implement them, and it does not fill a live JSON handler's cache.

If you declared [security requirements](operations.md#declare-security), include
their schemes in the export callback. `OpenAPIBuilder.Description` can also set
the document's Info description; this does not replace operation descriptions.

## Serve JSON

The JSON handler exposes endpoint names and schemas. If that information is
private, register it on a protected router or restricted listener, not an
unprotected public route. The handler adds no access controls.

To publish the contract on an appropriately protected `routes` router:

```go
routes.Get("/openapi.json", routes.OpenAPIHandler("Greeting API", "1.0.0"))
```

Serve that router through your [HTTP server](../http-networking/server.md).
A successful GET returns the document with `Content-Type: application/json`.
The registration has no operation callback, so it is omitted from its own export.

Document configuration callbacks run once when you construct `OpenAPIHandler`.
Generation happens on its first request, capturing all routes registered before
that request. The handler caches either the generated bytes or the generation
error. Later registrations do not invalidate it; construct another handler when
you need a different document. Construction does not compile or freeze the router.

This is an in-process generation cache, not an HTTP cache policy. The JSON handler
sets no `Cache-Control` header; configure that policy in your middleware if needed.
A generation failure returns through the router's ordinary
[error handling](../http-fundamentals/error-handling.md) on every request to that
handler, rather than retrying generation.

## Add Swagger UI

Swagger UI lets people browse the document and make API requests from their
browser. Protect its complete subtree as well as the JSON endpoint when the
contract is private; protecting only the HTML page leaves the JSON exposed.

Import `arkoren.dev/core/http/swagger` and construct a viewer for your JSON URL:

```go
viewer, err := swagger.Handler("/docs", "/openapi.json")
```

Invalid paths or URLs return an error. After checking `err`, register both the
base route and its subtree on the appropriately protected router:

```go
routes.Get("/docs", viewer)
routes.Get("/docs/{path...}", viewer)
```

The viewer serves `/docs` and `/docs/`, with assets beneath `/docs/`. Keep these
paths equal to the base path passed to the factory. Leave both registrations
unannotated: the exporter does not discover operations inside the UI, and an
annotated catch-all would fail export.

Once the server runs, opening `/docs/` makes the browser fetch `/openapi.json`.
The factory neither generates nor fetches that document. Pinned JavaScript and
CSS are bundled locally; the initializer disables the remote validator and
query-string configuration. The browser still needs access to the document and
to any API it calls, so bundled assets do not make a remote API available offline.

If the default tab title, `API documentation`, is too generic, use this alternative
construction:

```go
viewer, err := swagger.Handler("/docs", "/openapi.json", func(page *swagger.Builder) {
	page.Title("Greeting API")
})
```

The title is escaped as plain text; an empty title remains empty. Construction
callbacks run once, synchronously in order, even for invalid configuration. Nil
callbacks panic. Retaining the draft cannot change the completed handler, which
supports concurrent requests.

## Choose URLs

Prefer a root-relative document URL such as `/openapi.json` when the viewer and
document share an origin. HTTP(S) and relative URLs are also accepted. Relative
URLs resolve against the configured base path plus `/`, consistently whether the
page was opened at `/docs` or `/docs/`; request Host and forwarding headers do not
change that resolution.

The base path must be a clean absolute URL path with unreserved ASCII segments.
It cannot have a trailing slash unless it is `/`. Wildcards, escaped separators,
and queries are invalid. The document URL must be nonempty and cannot contain
credentials, fragments, scheme-relative URLs, whitespace, or backslashes.

The viewer serves only its page and named assets; unknown paths return `404`.
It accepts GET and HEAD, with no response body for HEAD. Other methods return
`405` with `Allow: GET, HEAD` if they reach the handler; router method matching can
reject them first. HTML and the initializer use `Cache-Control: no-store`;
pinned JavaScript and CSS use `public, max-age=3600`.

## Protect Documentation

A private contract needs protection at every entry point: the JSON route, viewer
page, and viewer subtree. Use your existing authentication middleware or a
[restricted listener](../application/health-and-readiness.md). Hiding an operation
from OpenAPI does not protect the live endpoint, and publishing its security
requirement does not enforce it.

The browser must authenticate its document fetch as well as API requests made
from the viewer. Swagger UI does not bypass API authentication, authorization,
CORS, or other middleware. If you choose a cross-origin document URL, the browser
policy and authentication mechanism must permit that fetch.

If you configure [Content Security Policy](../browser-security/secure-headers.md),
allow same-origin scripts, inline styles used by Swagger UI, and connections to
the document and API origins. Allow data images if you use the UI's embedded
images. No inline script permission is required.

[Diagnostic route tools](../diagnostics/capture-setup.md) are a separate operator
interface. They inspect registered routes; they do not mutate OpenAPI schemas or
turn REST endpoints into MCP tools. Publishing a contract does not replace an
[MCP server](../mcp/first-server.md).

For document export and settings, see the
[`router` API reference](https://pkg.go.dev/arkoren.dev/core/http/router).
For viewer configuration, see the
[`swagger` API reference](https://pkg.go.dev/arkoren.dev/core/http/swagger).


[HTML documentation](https://arkoren.dev/docs/guides/openapi/serving)
