Introduction to Swagger for CloudFormation and API Gateway

When I was writing my previous blog post about Introduction to CloudFormation for API Gateway, I noticed that CloudFormation also supports Swagger for API Gateway configuration. Curious about what such an implementation would look like in comparison to the previous solution, I decided to give it a go. Like the previous example, consider this blog post and related source code as a proof of concept. If you choose to implement a similar solution, I advise you to study the reference docs using the provided links. The accompanying source code to this blog post can be found in my GitHub repo.

Goal

The aim of this blog post is to re-implement the very same example as in the previous post, but this time use Swagger configuration instead of CloudFormation resources to configure the REST API. The business logic is an AWS Lambda function called GreetingLambda which has been configured with an appropriate execution role. If the event passed to the Lambda contains a name property, a JSON document with a greeting containing the name is returned. If not, the greeting Hello, World! is returned.

exports.handler = (event, context, callback) => {
  const name = event.name || 'World';
  const response = {greeting: `Hello, ${name}!`};
  callback(null, response);
};

When proxied by an API Gateway, it should be possible to perform a HTTP request with the name as a request parameter.

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting?name=Superman
{"greeting":"Hello, Superman!"}

CloudFormation Resources

Many of the CloudFormation resources will be left untouched from the previous implementation, so I will not describe them again (you will find them in the cloudformation.template). Let us focus on the differences instead.

API Gateway Rest API

The biggest change is to the AWS::ApiGateway::RestApi resource. By configuring the Body property it is possible to inline the entire REST API using Swagger’s JSON format. As a result, the CloudFormation template as a whole will be less verbose since some other CloudFormation resources can be deleted. That said, it is still a mouthful when you include the API Gateway Extensions to Swagger to handle the integration between the API Gateway and other AWS services.

"GreetingApi": {
  "Type": "AWS::ApiGateway::RestApi",
  "Properties": {
    "Name": "Greeting API",
    "Description": "API used for Greeting requests",
    "FailOnWarnings": true,
    "Body": {
      "swagger": "2.0",
      "info": {
        "version": "2016-08-18T18:08:34Z",
        "title": "Greeting API"
      },
      "basePath": "/LATEST",
      "schemes": ["https"],
      "paths": {
        "/greeting": {
          "get": {
            "parameters": [{
              "name": "name",
              "in": "query",
              "required": false,
              "type": "string"
            }],
            "produces": ["application/json"],
            "responses": {
              "200": {
                "description": "200 response"
              }
            },
            "x-amazon-apigateway-integration": {
              "requestTemplates": {
                "application/json": "{\"name\": \"$input.params('name')\"}"
              },
              "uri": {"Fn::Join": ["", [
                "arn:aws:apigateway:",
                {"Ref": "AWS::Region"},
                ":lambda:path/2015-03-31/functions/",
                {"Fn::GetAtt": ["GreetingLambda", "Arn"]},
                "/invocations"]
              ]},
              "responses": {
                "default": {
                  "statusCode": "200"
                }
              },
              "httpMethod": "POST",
              "type": "aws"
            }
          }
        }
      }
    }
  }
}

Configuration comments:

  • Body Root node of the Swagger configuration. Basically a JSON document that conforms to the Swagger 2.0 Specification.
  • /greeting Defines the greeting endpoint and its behavior, e.g. it accepts HTTP GET requests, it has an optional query parameter called name, it responds with 200 - OK and the response is in JSON format.
  • x-amazon-apigateway-integration A big part of the code consists of the configuration that is responsible for mapping and transforming the request and response from the RESTful endpoint to the specified AWS service. It contains several sub-properties, more than the ones listed below.
    • requestTemplates This part of the configuration transforms the incoming request before passing the result downstream då the Lambda function. In this example, the value of the name query parameter is transformed into a JSON object that has the format {"name": "Superman"}.
    • uri The backend endpoint, here exemplified by the GreetingLambda ARN.
    • responses Response status patterns from the backend service. Each key has then a statusCode property, which in turn must have a matching status code in the Swagger operation responses. Put differently, this is the HTTP status code that will be returned to the client. In the sample above, you will see that default is used as status pattern and 200 as status code, meaning that the client will always get a 200 - OK as a result, regardless if the Lambda call was successful or not. This is something you likely want to change in a production project.
    • httpMethod and type defaults to HTTP POST and aws when the API Gateway proxies AWS Lambda.
  • In addition to the x-amazon-apigateway-integration object the API Gateway Extensions to Swagger also have support for other properties such as custom authorization configuration.

Ref: AWS::ApiGateway::RestApi

Redundant CloudFormation Resources

By having all the RESTful endpoints declared in Swagger format, one can omit the AWS::ApiGateway::Resource and the AWS::ApiGateway::Method resources.

Test

Similarly as in the previous blog post, the API Gateway gets a root url of the https://[api-id].execute-api.[region].amazonaws.com format. If you create a stack based on the complete cloudformation.template you will see the exact url in the stack output. To verify that the stack works, you have to append the basePath and the /greeting path to it before issuing a HTTP GET request.

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting
{"greeting":"Hello, World!"}

You can also provide a request parameter:

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting?name=Superman
{"greeting":"Hello, Superman!"}

Considerations

  • In this example, the Swagger configuration was inlined in the CloudFormation Template. If you prefer to keep things separate one can upload a Swagger file to S3 and point the BodyS3Location property of the AWS::ApiGateway::RestApi to it. One advantage of using this property is that the file can be in either YAML or JSON format. Another benefit of keeping the Swagger configuration separate from the CloudFormation template is that you can pass the very same Swagger file to the client developers and they can use tools such as the Swagger Code Generator to generate client APIs and documentation. A minor disadvantage is of course that you need keep two files in sync when you deploy a new API Gateway Stage.
  • Swagger enables “contract first design”, i.e. you can define a RESTful API before any client or server business logic exists. It is an opinionated way of working and I usually prefer to develop the API and app incrementally. The good news is that Swagger can be used by both parties.
  • It is easy to prototype a new API Gateway using “point and click” in the AWS Console. When you are finished you can choose to Export an API from API Gateway, including the API Gateway Integration, using either a HTTP Get request or the AWS Console. Once you get hold of the Swagger file, you can paste it into your CloudFormation template or save it in an S3 bucket. Preferably, you take the opportunity to do some pruning in the process, for example you can replace any string that contains an AWS region with {"Ref": "AWS::Region"}, any string that contains a reference to another CloudFormation resource with {"Ref": "<logical resource id>"}, {"Fn::GetAtt": ["<Lambda logical id>", "Arn"]} for retrieving a Lambda ARN, etc.

Updated: