Pass additional arguments to Lambda handlers
A powerful feature of Serverless contracts is its ability to provide type-safe additional arguments to your lambda handler.
Why use additional arguments?
Lambda handlers typically interact with quite a lot of AWS services. This can be:
- to read information (ex: from DynamoDB or SSM Parameter Store)
- to write information (ex: to DynamoDB)
In any case, all the interactions that the Lambda has with other services can be considered as side-effects, meaning that they directly influence the Lambda's behavior. This makes the Lambda code quite hard to structure and test as it grows.
In fact, each side-effect make our Lambda code "impure" in terms of functional programing. A possible solution is to pass all the side-effects as arguments to our handler code. However, this is not natively possible on Lambda.
Serverless contracts natively embrace this idea and provide a simple way to make your lambda code pure again!
How to use additional arguments?
Define the handler with additional arguments
All Swarmion "runtime" contracts (such as ApiGatewayContract
and EventBridgeContract
) are compatible with the additional arguments feature. Let's take an example with ApiGatewayContract
.
Let's imagine that we want to pass it a sideEffect called getUser
with type (userId: string) => Promise<User>
. We could pass it to our Lambda
// handler.ts
import { getHandler, HttpStatusCodes } from '@swarmion/serverless-contracts';
import { getUser } from 'path/to/getUser';
import { ajvInstance } from 'libs/ajv';
const sideEffects = {
getUser, // (userId: string) => Promise<User>
};
const main = getHandler(
myContract,
ajvInstance,
)((
event,
_context, // will always be passed by Lambda
_callback, // will always be passed by Lambda
{ getUser }: typeof sideEffects = sideEffects,
) => {
const user = await getUser('toto'); // type-safe!
const { name, id } = user; // type-safe!
return { statusCode: HttpStatusCodes.OK, body: { name, id } };
});
There's quite a lot going on here, let's dive into it:
- ⚠️ you need to explicitly provide Typescript with the type of our additional argument (
typeof sideEffects
). Otherwise it would benever
! - ⚠️ you need to explicitly pass a default value to the additional argument (with
= sideEffect
). Otherwise, it will be undefined at runtime! - ⚠️ you need to take into account the
context
andcallback
arguments that will be passed to you handler at runtime by Lambda even if you don't use them!
Override the additional arguments in tests
Here is the real treat with this pattern: it become super easy to test you lambda behavior and abstract away the side-effects complexity.
Let's say you want to ensure that your lambda returns the name
and id
of the returned user, you could write a handler test:
// handler.test.ts
import { main } from './handler';
const mockEvent = {
// ...
};
const mockGetUser = vitest.fn(() => ({
statusCode: HttpStatusCodes.OK,
body: { name, id },
}));
const result = await main(
mockEvent,
mockContext, // fake context
() => null, // fake callback
{ getUser: mockGetUser }, // type-safe!
);
expect(result).toEqual({
statusCode: HttpStatusCodes.OK,
body: { name: 'Toto', id: 'toto' },
});
expect(mockGetUser).toHaveBeenCalledOnce();
expect(mockGetUser).toHaveBeenCalledWith('toto');
The call to main
here is type-safe, which means that if you change the sideEffects
in the handler definition, Typescript will enforce updating of the related tests.
This example shows that you can test the logic of the handler without worrying about the underlying implementation of the getUser
function!