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:

  1. Create S3 bucket if it not exists
  2. Enable static web hosting on S3
  3. Download Swager UI
  4. Export Swagger from API Gateway
  5. Upload everything to S3
I am working on linux so I will create shell script. If you are on windows feel free to do something similar with powershell. Create deploy.sh file, add shebang #!/bin/bash at the top, then make it executable with

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.