API Monitoring for the JAMStack

Originally published at https://www.checklyhq.com/guides/api-monitoring.

Application Programming Interfaces (APIs) are used throughout software to define interactions between different software applications. In this article we focus on web APIs specifically, taking a look at how they fit in the JAMStack architecture and how we can set up API monitoring in order to make sure they don’t break and respond fast.

diagram of jamstack web application
JAMSTACK APPLICATIONS HEAVILY RELY ON APIS

APIs and the JAMStack

The API calls might be aimed at internal services or at third-parties handling complex flows such as content management, authentication, merchant services and more. An example of third-party API could be Stripe, which acts as payment infrastructure for a multitude of businesses.

Given their importance in this new kind of web application, APIs both internal and external need to be tightly monitored, as failures and performance degradations will immediately be felt by the end-user.

API failures

  1. The endpoint is unresponsive/unreachable.
  2. The response is incorrect.
  3. The response time is too high.

All of the above can result in the application becoming broken for the end-user. This applies to internal APIs and, especially in the case of JAMStack applications, to third parties as well. API checks allow us to monitor both by mimicking the end-user’s own behaviour.

API checks

  1. An HTTP request.
  2. One or more assertions, used to specify exactly what the response should look like, and fail the check if the criteria are not met.
  3. A threshold indicating the maximum acceptable response time.

The more customisable the HTTP request is, the more cases can be covered, for example with authentication, headers and payloads.

It is worth noting that in real-world scenarios, requests do not happen in a vacuum: they are often handling data retrieved previously, possibly by earlier API calls. Therefore, some mechanism to gather this data and inject it into the request is often needed.

Let’s dive in deeper into each point.

Configurable HTTP request

  1. Method, like GET, PUT, POST, DELETE, etc
  2. Headers, like Accept, Authorization, Content-Type, Cookie, User-Agent, etc
  3. Query parameters
swagger api documentation screenshot
SWAGGER IS A POPULAR TOOL FOR GENERATING API DOCUMENTATION

Essentially, we are trying to craft a complete request for exact endpoint. Not only that, but we might want to have multiple requests set up to cover specific options or negative cases, too.

One such case can be where user-specified parameters such as pagination and timeframes might largely change the response. This is exemplified by the List Customers method in Stripe's Customer API, which we can use to query elements in very different ways, such as by just specifying a limit of results or asking for all results linked to a specific creation date. In this case, both of the following cases are worth monitoring:

curl https://api.stripe.com/v1/customers \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d created = 1616519668 \
-G
curl https://api.stripe.com/v1/customers \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d created=1616519668 \
-G

If we chose to set up a call using Javascript, for example, we could achieve the same call as in the first case above using axios:

Assertions

  1. Status code
  2. Headers
  3. Body

Let’s look at an example: creating a customer via the Stripe Customer API. Since we are not the API’s developers, we are assuming the result we get running call right now is correct and can be used to model our assertions. Let’s run the following curl command in verbose mode:

curl -v https://api.stripe.com/v1/customers \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d description = "My First Test Customer (created for API docs)"

Within the lengthy output we find the respose (in curl denoted by the ‘<’ symbol), and in it all the important details we need for our assertions.

First, we notice the successful status code:

< HTTP/2 200

After that, we can see the headers, which we might want to check for:

< content-type: application/json
< content-length: 1190
< access-control-allow-credentials: true
< access-control-allow-methods: GET, POST, HEAD, OPTIONS, DELETE
< access-control-allow-origin: *
< access-control-expose-headers: Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required
< access-control-max-age: 300
< cache-control: no-cache, no-store
< request-id: req_S9P5NqvZXzvvS0
< stripe-version: 2019-02-19
< x-stripe-c-cost: 0
< strict-transport-security: max-age=31556926; includeSubDomains; preload

And finally the response body, which we might want to inspect to ensure the right data is being sent back:

{ 
"id": "cus_JAp37QquOLWbRs",
"object": "customer",
"account_balance": 0,
"address": null,
"balance": 0,
"created": 1616579618,
[clipped]

We could expand on our previous code example by add adding an assertion library, such as chai’s or Jest expect:

We are now asserting against all three point mentioned above. We could of course go on and add additional assertions against both headers and body.

Response time thresholds

The easiest way to handle this requirement would be to assert that the specific response time be lower than a certain value, or even just set a timeout for our axios request by adding the timeout: 7500 property in the previously shown request config.

Instead of simply asserting against a specific response, we might want to set different thresholds: based on the nature of our service, a 2x slowdown might still leave it in what we define as an operational state, while a 10x one might not.

API monitoring best practices

Monitor every endpoint

  1. GET /user/:id
  2. PUT /user/:id

The above count as two separate endpoints, even though the URL is the same.

Cover key API parameters

Keep checks focused & independent

Scheduled global API checks

checkly dashboard with API checks
CHECKLY API CHECKS SHOWN ON A DASHBOARD

A Checkly API check is comprised of the following components.

Main HTTP request

checkly api creation screen
CHECKLY API CHECK CREATION

Assertions

checkly assertion creation screen
SETTING UP ASSERTIONS FOR OUR CHECK

In this example, we are checking against:

  1. The status code, expected to be 200.
  2. The id of one of the customers returned as part of the response’s JSON body. Here we could assert a specific value, but in this case we are happy with just verifying that the field is not empty.
  3. The value of the Content-Encoding header, expected to equal gzip.

Response time limits

checkly response time limit slider
CHOOSING RESPONSE TIME LIMITS

Setup and teardown scripts

setup and teardown methods on checkly
SETUP AND TEARDOWN METHODS ARE FULLY SCRIPTABLE USING NODEJS

Setup scripts run before our check and give us access to properties like the URL, headers and query parameters, enabling us to set up all the prerequisites for a successful request. Some examples could be:

  1. Fetching a token from a different API endpoint.
  2. Setting up test data on the target system.
  3. Formatting data to be sent as part of the request.

Teardown scripts run after the request has executed, bur right before the assertions. They are useful for manipulating the response (for example to remove sensitive information) or removing any test data on the target system.

Improving our monitoring

  1. Importing existing Swagger/OpenAPI specs or even cURL commands using built-in functionality.
  2. Defining our API checks as code to scale our setup while lowering maintenance needs.
  3. Combining our API checks with E2E monitoring for any website or web app service whose API we might be monitoring.

Banner image: “rover 200 framing line” by spencer_cooper is licensed under CC BY-ND 2.0