Interactive API documentation using Swagger UI deployed with Terraform

How to provide a clickable API documentation

Author's image
Tamás Sallai
5 mins

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:

/userGETPOST/user/<id>GETPUTDELETEAPI structure

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:

Swagger UI lists the available methods

It also allows calling the methods, even with a request body:

Send a POST with a request body

Generates copy-pastable Curl examples for the requests along with the response:

Swagger UI shows the response and a Curl example

And supports path parameters too:

Swagger UI 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!

Learn the services needed to build a serverless HTTP-based API on AWS from our free email course.

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 (Subresource Integrity) makes sure they are intact.

Here are the files needed:

Bucket list with index.html and api.yaml

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.

Manage Lambda functions with Terraform. Learn how from our video course.

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:

Swagger UI shows the dynamic server URL

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.

27 October 2020
In this article