Generating and serving#
This page covers producing the OpenAPI document from your app, at build time or from the command line, and then serving it, committing it and generating clients from it.
How generation works#
Most of the document is built at compile time from your annotations and types. Doc
comments aren't available to compiled code, so they're extracted by running
crystal docs over your source when the document is generated.
Generate where the source code is
Generation needs the crystal compiler, your src/ directory and shard.yml (for
crystal docs). Generate the document at build time, or during development. A
deployed binary without its source can't generate it.
From the command line#
The application template adds a
--docs flag:
crystal build src/app.cr -o app
./app --docs # print the YAML document
./app --docs --file=openapi.yml # save it to a file
Behind the flag is a single call that you can use in any app:
require "action-controller"
require "action-controller/server"
docs = ActionController::OpenAPI.generate_open_api_docs(
title: "My API",
version: "1.0.0",
description: "Everything you need to manage widgets",
)
File.write("openapi.yml", docs.to_yaml)
title and version are required. Any other
info object fields, such as
description, termsOfService or contact, can be passed as named arguments. The result
is a NamedTuple, so .to_json works too.
In Docker builds#
The template's Dockerfile generates the document in the build stage, where the source is available, and copies it into the final image:
# Generate OpenAPI docs and MCP tool descriptions while we still have source code access
RUN ./bin/app --docs --file=openapi.yml && \
./bin/app --mcp=mcp.yml
# ...
COPY --from=build /app/openapi.yml /openapi.yml
Serving the document#
Load the generated file at startup and return it from a route:
class Docs < AC::Base
base "/"
# generated during the docker build
OPENAPI = YAML.parse(File.exists?("openapi.yml") ? File.read("openapi.yml") : "{}")
# returns the OpenAPI representation of this service
@[AC::MCP(hide: true)]
@[AC::Route::GET("/openapi")]
def openapi : YAML::Any
OPENAPI
end
end
@[AC::MCP(hide: true)] keeps the document out of your MCP tools:
it's for people and code generators, not models.
Point Swagger UI, Redoc
or Scalar at /openapi to browse it.
Committing the document#
Many projects also commit the generated file (e.g. OPENAPI_DOC.yml). Reviewers can then
see API changes in pull requests, and clients can be generated from the repository.
Regenerate it whenever routes change:
crystal build src/app.cr -o app && ./app --docs > OPENAPI_DOC.yml && rm app
Generating clients#
Any OpenAPI tool can consume the document. For example, a TypeScript client with OpenAPI Generator:
npx @openapitools/openapi-generator-cli generate \
-i openapi.yml -g typescript-fetch -o ./client
Because the document is generated from the server code, regenerating the client after an API change gives you compile errors exactly where your frontend needs updating.
Troubleshooting#
| Symptom | Cause |
|---|---|
failed to obtain route descriptions via 'crystal docs' |
generation ran without the source code, crystal, or shard.yml in the working directory |
| A route is missing | it was defined with the macro DSL (get "/" do), so use an annotation, or it's an OPTIONS route, which isn't documented |
| No response schema | the method has no return type |
| A summary contains notes meant for developers | separate those notes from the doc comment with a blank line |
See also#
- Describing routes
- MCP setup, which uses the same build step for
mcp.yml - Deployment