Interactive API documentation using Swagger UI deployed with Terraform
How to provide a clickable API documentation
API documentation
When a backend provides an API how do you provide documentation for people who want to use it? It can be an informal process, using documents that describe the available paths, and how each of them should be called.
This can be an ad-hoc list of methods and paths:
- GET
/user
=> List users - POST
/user
=> Create a new user - GET
/user/<id>
=> Return a user by id - PUT
/user/<id>
=> Updates a user - DELETE
/user/<id>
=> Deletes a user
Maybe accompanied by a graphical representation:
The problem with this approach is that it's informal. People can read it and it communicates the intent, but it's different for every API.
OpenAPI (formerly Swagger) is a standardized format that describes how an API works. It captures the important parts of the documentation and defines a structure for it:
paths:
/user:
get:
# ...
post:
# ...
requestBody:
# ...
'/user/{userid}':
parameters:
- name: userid
in: path
required: true
schema:
type: string
delete:
# ...
The advantage of a standard format is that general-purpose tools can process it. One such tool is Swagger UI which provides interactive documentation for the API and allows easy experimentation in a familiar format.
It provides a list of methods, so it's easy to get an overview of how the API is structured:
It also allows calling the methods, even with a request body:
Generates copy-pastable Curl examples for the requests along with the response:
And supports path parameters too:
This provides developers an easy way to try out the API without first reading the informal documentation and figuring out how each part works.
Let's see how to deploy a live API with Swagger UI using Terraform!
Swagger UI webpage
The Swagger UI runs entirely on the client-side. It requires 2 Javascript and 1 CSS files, along with the API documentation YAML (or JSON). The frontend dependencies
can be hosted along with the index.html
or they can be referenced from a CDN. The latter provides an easier setup as the whole thing will be one file
(the index.html
) and using SRI makes
sure they are intact.
Here are the files needed:
The index.html
loads the swagger-ui-standalone-preset
, the swagger-ui-bundle
, and the swagger-ui.css
from cdnjs. It's a good practice to check for new versions and update them from time to time.
<head>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui-standalone-preset.min.js"
integrity="sha512-WI88XrK/8xukiZdnlwlGrcdIyD9qgNXL15LiWbVnq0qpgd/YzRiewFplb5VyRxsbwZf7wRU5BnkCeNP/OV5CEg=="
crossorigin="anonymous"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui-bundle.min.js"
integrity="sha512-7aNGLo3pjgERnsRoSSRrr8Xy6lX8QeKJG3sh8qAeKDvRCExTvDxG6IPRNrCoY0EZG9B5BzGWV5l0xK9DqSSu+w=="
crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui.min.css"
integrity="sha512-jsql70MmFqKJfWGCXmi3GHPP2q2oi3Ad+6PRQWNeo6df+rxKB07IuBvcCXSrpgKPXaikkQgEQVO2YrtgmSJhUw=="
crossorigin="anonymous" />
</head>
<body>
<div id="main"></div>
</body>
To initialize the UI call the SwaggerUIBundle
function, passing it the url
of the API documentation file and a DOM element.
window.addEventListener("DOMContentLoaded", () => {
const ui = SwaggerUIBundle({
url: "api.yaml",
dom_id: "#main",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout",
});
});
Bucket configuration
Since the Swagger UI and the API documentation YAML are static files, any web hosting works for them. The simplest is to put them into an S3 bucket and enable the bucket website endpoint.
resource "aws_s3_bucket" "bucket" {
force_destroy = "true"
website {
index_document = "index.html"
}
}
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.bucket.id
policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "${aws_s3_bucket.bucket.arn}/*"
}
]
}
POLICY
}
The above configuration creates a bucket, enables the website endpoint, then attaches a bucket policy to allow anonymous access ("Principal": "*"
). This
gives a URL in the form of http://<bucket>.s3-website.<region>.amazonaws.com
.
Notice that it uses unencrypted HTTP and S3 does not support HTTPS. If you want to expose the API documentation via HTTPS (which you should), use CloudFront in front of S3.
Dynamic server URL
How does Swagger UI know where is the API? It is in the YAML file, in the servers
section.
But when, for example, the same API is managed by Terraform and the URL is different for every deployed stack, it needs to be set dynamically.
Terraform supports the templatefile
function that allows defining values and inserts them into the document:
resource "aws_apigatewayv2_api" "api" {
# ...
}
resource "aws_s3_bucket_object" "api_object" {
# ...
content = templatefile("api.yml", {api_url = aws_apigatewayv2_api.api.api_endpoint})
}
In the OpenAPI YAML, use the placeholder for the server URL:
servers:
- url: ${api_url}
When it is deployed, the YAML contains the URL of the API Gateway:
Files
To put files into a bucket, use the aws_s3_bucket_object
resource. The index.html
has a text/html
content type, so that the browser
shows it as a webpage. Then the API document is templated with the API Gateway URL.
resource "aws_s3_bucket_object" "object" {
key = "index.html"
content = file("index.html")
bucket = aws_s3_bucket.bucket.bucket
content_type = "text/html"
}
resource "aws_s3_bucket_object" "api_object" {
key = "api.yaml"
content = templatefile("api.yml", {api_url = aws_apigatewayv2_api.api.api_endpoint})
bucket = aws_s3_bucket.bucket.bucket
}
Output
To export the URL of the Swagger UI, use an output with the website_endpoint
attribute of the bucket:
output "url" {
value = aws_s3_bucket.bucket.website_endpoint
}
CORS configuration
The above setup hosts the Swagger UI in a different domain than the API itself, which means CORS (Cross-Origin Resource Sharing).
If you use an API Gateway HTTP API then it supports the cors_configuration
block where you can define the CORS-related headers:
resource "aws_apigatewayv2_api" "api" {
# ...
cors_configuration {
allow_origins = ["*"]
allow_methods = ["GET", "POST", "PUT", "DELETE"]
allow_headers = ["Content-Type"]
}
}
Some calls require a preflight request that is an OPTIONS and the browser requires it to have a success status code. Make sure to return a 200 response from the API:
if (method === "OPTIONS") {
return {
statusCode: 200,
};
}
Another solution is to host the Swagger UI on the same domain as the API. This can mean that the API returns the index.html
and the API YAML files,
or you can use CloudFront to bring everything under one domain.
Conclusion
OpenAPI provides a standard for API documentation and tools are available to process it. Swagger UI is a project that turns the API definition into an interactive documentation page where developers can get familiar with the API quickly and can experiment with it.
When Terraform manages the API it needs to wire the API URL and the documentation together and expose the website for the browser. Using an S3 bucket website
and the templatefile
function it is possible to manage the API endpoint and its documentation together.