loading...
Cover image for Mock a RESTful API with AWS APIGateway and MockIntegration

Mock a RESTful API with AWS APIGateway and MockIntegration

dvddpl profile image Davide de Paolis ・5 min read

Have you ever wondered how you can create a Mocked RestAPI if your Application runs serverless with API Gateway and Lambda?

Of course, you can implement the whole feature locally and have a mocked server locally, but what if you want to deploy your application, and have it tested by QA and checked by stakeholders?.

Mock the Lambda

The first approach could be having the API Gateway point to one or more lambdas that have hardcoded responses. That´s not bad, but you will pay for every Lambda execution, even though it is just a mock, and if you have many endpoints (and if your infrastructure will be based on Single purposed Lambdas, it could be very boring and time-consuming to set up all the mocks).

Mock the Integration

You can speed up the process, and save money, using MockIntegration!

You can configure a specific Resource, or Method, or the entire API with few lines of code (for the CDK setup) and no Lambda at all!

Imagine you already have a Gateway API set up with a specific endpoint GET Products so that a Lambda function loads your products from DB and return them.

const myApi = new apigateway.RestApi(this, 'MyAwesomeAPI', {
            restApiName: `my-awesome-api`, // <--- this is the name of the gateway api in Console
            description: `RestAPI for my awesome app`
        })

const productsIntegration = new apigateway.LambdaIntegration(productsLambdaHandler)

myApi.root.addResource("products").addMethod('GET', productsIntegration)
Enter fullscreen mode Exit fullscreen mode

Now imagine that you need to implement a feature to search for a specific user. Requirements for the backend are still unclear but you want to start implementing the frontend so that it invokes the API and shows the result, or an error if the user is not found.
You can just add a MockIntegration for that specific resource/method, like this:

// GET my-api.com/users/123456

const mockedResource = myApi.root.addResource("users").addResource('{userId}', {
            defaultCorsPreflightOptions: {
                allowOrigins: ['*'],
                allowCredentials: true
            }
        })

mockedResource.addMethod(
        'GET',
        findPlayerMockIntegration,
        findPlayerMethodOptions
    )

const findUserMockIntegration = new MockIntegration({
    passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES,
    requestTemplates: {
        'application/json': `{
                   #if( $input.params('userId') == 999999999)
                          "statusCode" : 404
                    #else
                           "statusCode" : 200
                    #end
                }`
    },
    integrationResponses: [
        {
            statusCode: '200',
            responseTemplates: {
                'application/json': ` 
                           { "name": "John",
                             "id": input.params('playerId'),
                             "surname": "Doe", 
                             "sex": "male",
                             "city": "Hamburg"
                             "registrationDate": 1598274405
                           }`
            }
        },
        {
            statusCode: '404',
            selectionPattern: '404',
            responseTemplates: {
                'application/json': `{"error": "Player ($input.params('userId')) not found"}`
            }
        }
    ]
})


const findPlayerMethodOptions = {
    methodResponses: [
        {
            statusCode: '200',
            responseModels: {
                'application/json': Model.EMPTY_MODEL
            }
        },
        {
            statusCode: '404',
            responseModels: {
                'application/json': Model.ERROR_MODEL
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

As you can see there, you can define 2 types of responses, one for a player that was found, and another one (404) for a player that wasn´t found. In the integration response you can return whatever json you like.

This partial mock is very easy to set up and very handy when you just need a specific endoint which is still missing in your backend, but what if you don't know yet much about the API and you just want to start building the full stack and iterate quickly?

You want the client to be able to invoke any possible method or resource without getting the weird and misleading Error Missing Authentication Token that you get if you try invoking a non-existing endpoint on your APIGateway, or you want to display a specific message for Maintenance or UnderDevelopment.

A Greedy Proxy is what is needed here.

Mock all the things

const proxyMethodOptions = {
    methodResponses: [
        {
            statusCode: '501',
            responseModels: {
                'application/json': Model.ERROR_MODEL
            }
        }
    ]
}

const proxyMockIntegration = new MockIntegration({
    passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES,
    requestTemplates: {
        'application/json': JSON.stringify({
            statusCode: 501
        })
    },
    integrationResponses: [
        {
            statusCode: '501',
            responseTemplates: {
                'application/json': JSON.stringify({
                    message: 'Mock for this resource/method not yet implemented'
                })
            }
        }
    ]
})

 const proxiedResource = myApi.root.addProxy({
        anyMethod: false, 
        defaultCorsPreflightOptions: {
            allowOrigins: ['*'],
            allowCredentials: true
        },
        defaultIntegration: proxyMockIntegration,
        defaultMethodOptions: proxyMethodOptions
    })
// for some reason the defaultMethodOptions are never applied but must be specified again afterwards (https://github.com/aws/aws-cdk/issues/8453)
    proxiedResource.addMethod('ANY', proxyMockIntegration, proxyMethodOptions)

Enter fullscreen mode Exit fullscreen mode

Seeing the code now it does not sound like a big deal, but it was not super straightforward to get there.
First, the documentation available is always a bit daunting.
Especially when it comes to move from playing around with the console to writing Infrastructure As Code with Serverless/Terraform or like in our case AWS CDK: Examples are mostly for the UI console or CLI and CDK docs are not always super clear and you need to dig into CloudFormation docs too.

Where to go from here

I really suggest you check this amazing article from Alex De Brie that gives you lots of insights about APIGateway and introduce you to the Velocity Templates and how to manipulate the request and responses of the Gateway API, the topic of Velocity Templates is even wider ( because they can be used to a big extent to manipulate input and outputs of other HTTPIntegration. ( like when you want to kind of proxy another external api or a legacy api ).

For a simple mock I wouldnt go too deep into the logic of Velocity templates, and if you really need dynamic data being mocked, then well, yes, I would rely on a Lambda and Faker.js

The amount of logic you put in your mapping templates comes down to your needs and your personal preferences. If the backend integration is not one that you control writing a complex VTL template may be your only option. On the other hand, if you control your integration, you may want to do the body transformations in your endpoint’s logic where it can be easily tested using your native tooling.

Nevertheless, in a couple of circumstances - mainly quick iteration in the development of a fronted application, and to create a mock backend to be used in integration tests ( so that we won´t spam a 3rd party API) we found mock integration really handy and easy to implement.

Although easy, there were some pain points, so I want to share some tips/info about things you might want to pay attention to.

Always make sure that the status code in your integration response is the same as the status code in MethodResponses for MethodOptions otherwise you will get a Execution failed due to configuration error: Output mapping refers to an invalid method response: {The wrongly configured error code}

const mockedMethodOptions = {
    methodResponses: [{
        statusCode: "501", responseModels: {
            "application/json": Model.ERROR_MODEL
        }
    }]
}


const myMockIntegration = new MockIntegration({
    passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES,
    requestTemplates: {
        "application/json": JSON.stringify({
            statusCode: 200
        })
    },
    integrationResponses: [{
        statusCode: "501",
        responseTemplates: {
            "application/json": JSON.stringify({
                message: "Work in progress"
            })
        },
    }]
})
Enter fullscreen mode Exit fullscreen mode

Additionally, the StatusCode in IntegrationResponse has to be a string, but in the Stringified RequestTemplate, status code MUST be a number:

            requestTemplates: {
                "application/json": JSON.stringify({
                    statusCode: 200  <-- this must be a number
                })
            },

            integrationResponses: [{
                statusCode: "503", <-- this must be a string
                responseTemplates: {
                    "application/json": JSON.stringify({
                        message: "Under Maintenance"
                    })
                },
            }]
Enter fullscreen mode Exit fullscreen mode

When adding a Proxy with MockIntegration to a RestAPI resource the ResponseModel of the MethodResponse is ignored in the Configuration, even though it is specified under defaultMethodfOptions.
See https://github.com/aws/aws-cdk/issues/9791

You can add some dynamic behaviour to your mock by acting on the parameters you are passing. It requires a bit of hardcoding but still does the trick.

In the RequestTemplate read the params and immediately return a response without going throught the integration ( in our case is mocked but - just to get the idea)

{
#if ($input.params('gameId') == "0")
    "statusCode" : 404
#else
    "statusCode": 200
#end
}
Enter fullscreen mode Exit fullscreen mode

Remember: the statusCode you return here will be used to determine which integration response template to use.

Hope it helps

Discussion

pic
Editor guide