Skip to main content

Creating an Adapter

As we saw in Introduction, you are already familiar with adapter methods.

In this chapter, we will create a new adapter to implement the AWS Api Gateway V2 that has the request and response in the following format:

Request and Response

request.json
{
"version": "2.0",
"routeKey": "$default",
"rawPath": "/my/path",
"rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
"cookies": [
"cookie1",
"cookie2"
],
"headers": {
"header1": "value1",
"header2": "value1,value2"
},
"queryStringParameters": {
"parameter1": "value1,value2",
"parameter2": "value"
},
"requestContext": {
"accountId": "123456789012",
"apiId": "api-id",
"authentication": {
"clientCert": {
"clientCertPem": "CERT_CONTENT",
"subjectDN": "www.example.com",
"issuerDN": "Example issuer",
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
"validity": {
"notBefore": "May 28 12:30:02 2019 GMT",
"notAfter": "Aug 5 09:36:04 2021 GMT"
}
}
},
"authorizer": {
"jwt": {
"claims": {
"claim1": "value1",
"claim2": "value2"
},
"scopes": [
"scope1",
"scope2"
]
}
},
"domainName": "id.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "id",
"http": {
"method": "POST",
"path": "/my/path",
"protocol": "HTTP/1.1",
"sourceIp": "IP",
"userAgent": "agent"
},
"requestId": "id",
"routeKey": "$default",
"stage": "$default",
"time": "12/Mar/2020:19:03:58 +0000",
"timeEpoch": 1583348638390
},
"body": "Hello from Lambda",
"pathParameters": {
"parameter1": "value1"
},
"isBase64Encoded": false,
"stageVariables": {
"stageVariable1": "value1",
"stageVariable2": "value2"
}
}
response.json
{
"isBase64Encoded": true,
"statusCode": 201,
"headers": { "headername": "headervalue" },
"multiValueHeaders": { "headername": ["headervalue", "headervalue2"] },
"body": "Done"
}

Creating the class

First, we need to create our new adapter class, let's define it as:

api-gateway-v2.adapter.ts
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda/trigger/api-gateway-proxy';
import { AdapterContract } from '@h4ad/serverless-adapter/contracts';

export class ApiGatewayV2Adapter implements AdapterContract<APIGatewayProxyEventV2, Context, APIGatewayProxyStructuredResultV2> {}

The interface we need to implement is AdapterContract which takes 3 generic arguments: the event sent by the event source, the context of the serverless environment, and the response that the event source understands.

Implementing the getAdapterName method

This is the easiest method to implement, we can do it as follows:

api-gateway-v2.adapter.ts
public getAdapterName(): string {
return ApiGatewayV2Adapter.name;
}

Implementing the canHandle method

When we implement this method, we must study the request event to know which properties are always sent by the event source, in case of AWS Api Gateway V2, the most important ones are requestContext and version and checking that the value of version is equal to 2.0.

This way, we can implement if the event was sent from AWS Api Gateway V2 as follows:

api-gateway-v2.adapter.ts
public canHandle(event: unknown): event is APIGatewayProxyEventV2 {
const apiGatewayEvent = event as Partial<APIGatewayProxyEventV2> & {
version?: string;
};

// this basically will verify if:
// - if event has requestContext
// - if event has version property equal to 2.0
// if both are true, then we do double bang
// just to make sure we return boolean
return !!(
apiGatewayEvent?.requestContext && apiGatewayEvent.version === '2.0'
);
}

Implementing the getRequest method

In this method, we need to return the AdapterRequest interface, to understand better what we need to return, let's deep into to know more about them.

method

The HTTP Method to use to create the request to the framework.

In AWS Api Gateway V2 we can take the method from APIGatewayProxyEventV2, accessing the method property inside the http object that is inside the requestContext, the code will look like this:

api-gateway-v2.adapter.ts
const method = event.requestContext.http.method;
tip

In some cases, you may not have this information from events such as SQS, in these specific cases you need to provide a way to control the path, we recommend you provide a default option and provide a way for the user to customize, such as the implementation of AWS SQS.

path

The path to use to create the request to the framework.

In the AWS Api Gateway V2, we can grab the path from APIGatewayProxyEventV2, acessing the property rawPath and combining with rawQueryString.

Fortunately, this operation is so common that we provide a function to help you with this operation, see above:

api-gateway-v2.adapter.ts
import { getPathWithQueryStringParams } from '@h4ad/serverless-adapter';

// ...
// inside the function `getRequest`
const path = event.rawPath;
const queryParams = event.rawQueryString;

const pathWithQueryParams = getPathWithQueryStringParams(path, queryParams);
info

This implementation deviates from the original implementation for educational purposes only. See the original implementation here.

tip

In some cases, you may not have this information from events such as SQS, in these specific cases you need to provide a way to control the path, we recommend you provide a default option and provide a way for the user to customize, such as the implementation of AWS SQS.

headers

The headers to use to create the request to the framework.

In the AWS Api Gateway V2, we can grab the headers from APIGatewayProxyEventV2, acessing the property headers.

But not only need to pass the property headers, we need to take care of headers not being like "accept-lang": ['pt-BR', 'en-US'], so we will use the helper function getFlattenedHeadersMap.

const headers = getFlattenedHeadersMap(event.headers, ',', true);

// this is a implementation detail for api gateway v2,
// the cookies is sent from another property instead being
// sent inside headers property
if (event.cookies)
headers.cookie = event.cookies.join('; ');
tip

In some cases, you may not have this information from events such as SQS, in these specific cases you can just mock the headers with default values, such as the implementation of AWS SQS.

body

The body as buffer to use to create the request to the framework

Well, the body actually can be anything you want, sometimes you will receive a JSON (eg: Api Gateway), Base64 or just plain javascript objects (eg: AWS SQS).ts

In the AWS Api Gateway V2, we can grab the body from APIGatewayProxyEventV2, acessing the property body and use the property isBase64Encoded to determine if the body is base64.

To help to transform the body from JSON or base64 to Buffer, we have the helper getEventBodyAsBuffer.

The code will look like this:

let body: Buffer | undefined;

if (event.body) {
const [bufferBody, contentLength] = getEventBodyAsBuffer(
event.body,
event.isBase64Encoded,
);

body = bufferBody;
headers['content-length'] = String(contentLength);
}
important

You need to set the content-length header to the value returned by the getEventBodyAsBuffer function.

remoteAddress

The remote address (client ip) to use to create the request to the framework

In the AWS Api Gateway V2, we can grab the remote address from APIGatewayProxyEventV2, acessing the property requestContext.http.sourceIp.

const remoteAddress = event.requestContext.http.sourceIp;

host and hostname

Actually these two properties are not used by the library, so you can just ignore.

Implementing the getResponse method

Maps the response of the framework to a payload that serverless can handle.

In other words, do you remember the json from the answer at the beginning of this tutorial?

So you need to return this json and let's see how we can map your function to this response:

api-gateway-v2.adapter.ts
public getResponse({
headers: responseHeaders,
body,
isBase64Encoded,
statusCode,
response,
}: GetResponseAdapterProps<APIGatewayProxyEventV2>): APIGatewayProxyStructuredResultV2 {
const headers = getFlattenedHeadersMap(responseHeaders);
const multiValueHeaders = getMultiValueHeadersMap(responseHeaders);

// I removed content encoding checks for learning purposes only
// but in the original version we need to check more things here.

const cookies = multiValueHeaders['set-cookie'];

if (headers) delete headers['set-cookie'];

return {
statusCode,
body,
headers,
isBase64Encoded,
cookies,
};
}

Why did I put all the code? Because in the response, I don't have much to explain because each adapter will have your own implementation for this function.

AWS SQS for example, they don't need to return anything, so the implementation is:

sqs.adapter.ts
public getResponse(): IEmptyResponse {
return EmptyResponse;
}

So look closely at the event source documents you are creating the adapter for and try to map as many properties as possible to the properties that the library sends you.

tip

You can check the GetResponseAdapterProps to check which data you have from the response of the framework.

Implementing the onErrorWhileForwarding method

When an error occurs while forwarding the request to the framework.ts

Well, errors can happen and we need to have a way to handle those cases to not messup and not have any information about the error.ts

So this method is used to those specific cases, to ensure how to map the error received to event source deal properly.

In the AWS Api Gateway V2, we handle like this:

api-gateway-v2.adapter.ts
public onErrorWhileForwarding({
error,
delegatedResolver,
respondWithErrors,
event,
log,
}: OnErrorProps<
APIGatewayProxyEventV2,
APIGatewayProxyStructuredResultV2
>): void {
const body = respondWithErrors ? error.stack : '';
const errorResponse = this.getResponse({
event,
statusCode: 500,
body: body || '',
headers: {},
isBase64Encoded: false,
log,
});

delegatedResolver.succeed(errorResponse);
}

If the respondWithErrors flag is enabled, we will return the entire stack trail in the response, so the user will get a 500 error with enough information.

important

When implementing this method, you must call delegatedResolver.succeed or delegedResolver.fail, or your request will not be resolved.

Other adapters may fail when an error occurs, the AWS SQS adapter does that.

sqs.adapter.ts
public onErrorWhileForwarding({
error,
delegatedResolver,
}: OnErrorProps<SQSEvent, IEmptyResponse>): void {
delegatedResolver.fail(error);
}

Well Done!

Now you can create your own adapters to plug with any event source that you want.

tip

If you get lost while building your adapter, see the source code of other adapters to get some ideas on how to implement your own adapter, you can see the code here.