Documenting Serverless API
There are many great languages and tools in the wild that can help you to create and document API's. In this article we are going to focus on AWS API Gateway, serverless framework, serverless documentation plugin, Swagger UI and some shell scripting. Let's start with the quick introduction about these tools from their offical documentation.
- Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.
- The Swagger UI is an open source project to visually render documentation for an API defined with the OpenAPI (Swagger) Specification.
- Serverless framework provides a powerful, unified experience to develop, deploy, test, secure and monitor your serverless applications.
- Serverless documentation plugin adds support for AWS API Gateway documentation and models
- A shell script is a computer program designed to be run by the Unix shell, a command-line interpreter.
Knowing all of this we can start implementing our solution. Install serverless framework then start a new project. In this example I will use NodeJS but you can use whatever lambda supports.
npm i -g serverless
sls create --template aws-nodejs --path document-api-example
cd document-api-example
npm init -y
Install serverless documentation plugin.
npm i serverless-aws-documentation --save-dev
We can now update serverless.yml by adding plugin and two endpoints that we are going to implement and document.
service: document-api-example
provider:
name: aws
runtime: nodejs10.x
plugins:
- serverless-aws-documentation
functions:
get:
handler: handler.get
events:
- http:
path: hello
method: get
cors: true
post:
handler: handler.post
events:
- http:
path: hello
method: post
cors: true
To implement get and post endpoints we need to update handler.js. GET will just say hi, while POST will say hello to the name sent in the request body, or return an error if body cannot be parsed.
const response = (statusCode, body) => {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin' : '*'
},
body: JSON.stringify(body)
};
}
const get = async () => response(200, { message: 'Hi from get method!' });
const post = async (event) => {
try {
const { name } = JSON.parse(event.body);
if(!name) throw {
statusCode: 400,
message: 'bad input'
}
return response(200, { message: `hello ${name}` });
} catch(err) {
const { statusCode=500, message='server error' } = err;
return response(statusCode, { message });
}
}
module.exports = {
get,
post
}
Now we are ready to deploy these two endpoints.
sls deploy --stage dev --region eu-west-1
If all goes well, serverless response will be something like this:
endpoints:
GET - https://z5k7tcjhig.execute-api.eu-west-1.amazonaws.com/dev/hello
POST - https://z5k7tcjhig.execute-api.eu-west-1.amazonaws.com/dev/hello
Write down first part of the url z5k7tcjhig, you will need it later. This is the API Gateway ID. Obviously your ID will be different. Next thing that we need to do is to write documentation. We are going to use serverless documentation plugin for that. So let's update again our serverless.yml file with hello and response models.
service: document-api-example
provider:
name: aws
runtime: nodejs10.x
plugins:
- serverless-aws-documentation
custom:
documentation:
api:
info:
version: '1.0.0'
title: Document API Example
description: “Documentation is a love letter that you write to your future self.”
models:
-
name: "Hello"
description: "Hello post object"
contentType: "application/json"
schema:
type: "object"
properties:
name:
type: "string"
-
name: "HelloMessage"
description: "Hello post object"
contentType: "application/json"
schema:
type: "object"
properties:
message:
type: "string"
-
name: ErrorResponse
contentType: "application/json"
schema:
type: object
properties:
message:
type: string
statusCode:
type: number
functions:
get:
handler: handler.get
events:
- http:
path: hello
method: get
cors: true
documentation:
summary: "GET will say hi"
description: "Get hello from our GET endpoint"
tags:
- Hello
method: get
path: hello
methodResponses:
-
statusCode: "200"
responseModels:
"application/json": "HelloMessage"
-
statusCode: "500"
responseModels:
"application/json": "ErrorResponse"
post:
handler: handler.post
events:
- http:
path: hello
method: post
cors: true
documentation:
summary: "POST hello"
description: "POST will say hi to the name posted in body"
tags:
- Hello
method: get
path: hello
requestModels:
"application/json": "Hello"
methodResponses:
-
statusCode: "200"
responseModels:
"application/json": "HelloMessage"
-
statusCode: "400"
responseModels:
"application/json": "ErrorResponse"
-
statusCode: "500"
responseModels:
"application/json": "ErrorResponse"
According to AWS Docs we can export swagger file from our API. We can also utilize S3 to host Swager UI. So what is left is to write script that will:
- Create S3 bucket if it not exists
- Enable static web hosting on S3
- Download Swager UI
- Export Swagger from API Gateway
- Upload everything to S3
chmod +x ./deploy.sh
Our script is going to take 3 input parameters: S3 Bucket ( -b or --bucket ), AWS Region ( -r or --region ) and API Gateway Id ( -g or --gateway). If any of these parameters is not supplied script will exit.
To check presence of the bucket we will use head-bucket. If bucket does not exists it will be created. Next is a command aws s3 website that will enable static web site hosting in our bucket.
After setting up bucket we can clone swagger ui from the repository, replace reference to example file with our file name in the index.html of the UI.
We will export swagger file to api.json from API Gateway using cli again. After export we will upload this file to our bucket and print out url to our static website.
#!/bin/bash
# get variables from params
for i in "$@"
do
case $i in
-b=*|--bucket=*)
S3_BUCKET="${i#*=}"
shift
;;
-r=*|--region=*)
REGION="${i#*=}"
shift
;;
-g=*|--gateway=*)
API_GATEWAY_ID="${i#*=}"
shift
;;
esac
done
# exit if no input params are supplied
S3_BUCKET=${S3_BUCKET:?--bucket or -b parameter. This is the S3 bucket which will host Swagger UI. }
REGION=${REGION:? --region or -r parameter is required. This is an AWS Region.}
API_GATEWAY_ID=${API_GATEWAY_ID:? --gateway or -g parameter. This is our API Gateway Id.}
# check if bucket exists /dev/null prevents error
if aws s3api head-bucket --bucket $S3_BUCKET 2>/dev/null;
then
echo 'bucket exists enable hosting';
else
echo 'no bucket - create it';
aws s3api create-bucket --bucket $S3_BUCKET --region $REGION --create-bucket-configuration LocationConstraint=eu-west-1
fi
# enable static website hosting on the S3 bucket
aws s3 website s3://$S3_BUCKET/ --index-document index.html --error-document error.html
# remove swagger-ui folder then clone repo for the latest version
rm -rf swagger-ui
git clone https://github.com/swagger-api/swagger-ui.git
# replace reference inside index.html from https://petstore.swagger.io/v2/swagger.json to api.json
sed -i 's/https:\/\/petstore.swagger.io\/v2\/swagger.json/api.json/g' ./swagger-ui/dist/index.html
# upload swagger ui to S3 bucket
aws s3 cp ./swagger-ui/dist s3://$S3_BUCKET/ --recursive --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers
# export swagger doc from API Gateway
aws apigateway get-export --parameters extensions='apigateway' --rest-api-id $API_GATEWAY_ID --stage-name dev --export-type swagger ./api.json --region $REGION
# upload exported file to S3
aws s3 cp ./api.json s3://$S3_BUCKET/ --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers
# cleanup
rm -rf swagger-ui
# print your bucket url
echo http://$S3_BUCKET.s3-website-$REGION.amazonaws.com
Finally execute this script from command line but don't forget to pass required params. On my account I did it like so:
./deploy.sh --bucket=document-api-example --region=eu-west-1 --gateway=z5k7tcjhig
Replace bucket, region and gateway with your values. Every time you deploy API's on AWS you can run this script to update documentation. You can see live demo here.
As usual source code is available on GitHub. If you have any questions feel free to use GitHub issues.