Building a Stripe Webhook Server in Typescript with AWS CDK - Part 2
Implementation & configuration
If you missed part 1 on defining your CDK stack you can catch up here.
Part 2 - Coding our Lambda Function
We now have a CDK stack that will generate the Cloudformation templates we need and deploy the resources into AWS. You can test this by running `cdk deploy` but it's fairly pointless at this stage as our Lambda function is empty and won’t do anything. So let us get to it.
First off we need a couple of extra dependencies; `stripe` and `aws-lambda`. We only require a couple of types from the `aws-lambda` package to define our function signatures so we can add this package as a dev dependency.
From the project root run:
yarn add stripe && yarn add -D @types/aws-lambda
Next open the file at `lib/handler/index.ts`, define a function and export it with the name `handler`:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import Stripe from "stripe";
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {}
We have imported the 2 type definitions from the `aws-lambda` package and used them to describe the incoming event object and for the expected result. These will help us to correctly reference our event attributes and return a correctly shaped response.
Next in the body of our handler let's set up a try/catch and do a bit of basic validation and return some correctly formed responses.
try {
//validation
if (!event.body) throw new Error("No request body!");
if (!process.env.SECRET_KEY || !process.env.WEBHOOK_SECRET)
throw new Error("Missing required env");
//do more stuff here...
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: `Handled event`
}
} catch (err) {
return {
statusCode: 501,
headers: { "Content-Type": "text/plain" },
body: err.message || "Internal server error"
}
}
To do initial validation we are just checking that the request event has a body and that the Lambda has been correctly configured with the required environment variables.
Now we come to the interesting bit. In the body of our try/catch we need to verify the signature that Stripe sends in the request headers. This protects our endpoint from unauthorised requests and ensures the integrity of the event body so we can trust what it contains.
Drop the following block into the body of your try/catch under the validation:
const stripe = new Stripe(process.env.SECRET_KEY, {
apiVersion: "2020-08-27",
});
const validEvent: Stripe.Event = stripe.webhooks.constructEvent(
event.body,
event.headers["Stripe-Signature"],
process.env.WEBHOOK_SECRET
);
Here we are instantiating the Stripe SDK with the secret key that we placed into the runtime environment of the Lambda. Then using the SDK we attempt to construct a `Stripe.Event` object by providing the event body, the signature Stripe sends in the headers and the webhook secret that is also retrieved from the runtime environment. If this fails the error will be caught by our catch and bubbled up into the response body.
The `validEvent` object will contain 2 important attributes; `data` and `type`. The value of `type` will be from the list of valid event types that Stripe sends and `data` will contain the event object. Documentation of event types and the associated objects and their attributes are described here..
A lot of example code in other tutorials will suggest a switch statement to handle the different event types. I am not a fan of this pattern as there is a lot of possible events to handle which will make this function bloated, hard to read and maintain and difficult to test. I prefer to create a separate file to register handler functions which we can call `handlers`.
Create a new file in the handler directory called `handlers.ts` and drop in the following code block:
const Handlers = {
//Example event handler
"customer.subscription.updated": async (data) =>
console.log("Sub Updated", data),
};
export default Handlers;
We can now register our event handlers by adding new functions to this object by using the event type as the object key.
Now let's go back to our `index.ts` file and integrate our handlers with the following import:
import handlers from "./handlers";
And the following block underneath the creation of our `validEvent`:
//pass data into the event handler if registered
const { data, type } = validEvent;
if (handlers[type] instanceof Function) {
handlers[type](data, stripe);
}
Here we are checking if there is a function called by the event type and if it exists calling it by passing the data and our stripe object.
Note: It's likely to have occasions where further calls to the Stripe API will be required in your handlers. For this reason we pass our already configured Stripe object as a convenience.
You should now see Typescript complain a lot about missing types:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
We need to correct this so that Typescript will stop complaining. Let's go back to our handlers file and have a look at what needs to be done.
First off we can type our Handlers object like so:
const Handlers: { [key: string]: Function } = { ... }
This will define the object as having keys of type `string` and values of type `Function`. Check out your index.ts and the type errors should have gone away. However we are not done yet. You will notice our handler function signature is highlighted in red with the warning “Parameter `data` implicitly has `any` type”. There is a couple of ways to solve this problem but the way I approached it is to define a new type for our functions that looks like this:
import Stripe from "stripe";
type HandlerFunction = (
data: Stripe.Event.Data.Object,
stripe: Stripe
) => Promise<void>;
This defines a type that accepts the 2 arguments we expect to send and returns an empty promise. We can now update the Handlers object type like so:
const Handlers: { [key: string]: HandlerFunction } = { ... }
All your typing errors should now have gone away and your code should compile without complaint. We can check by running our unit tests again and checking they are still green.
Configuring your environment
To set up our configuration we have 2 options. To pass environment variables into Node at the CLI or to use a `.env` file (loaded into Node using the dotenv package). I use a combination of both by putting the Stripe keys (long and unwieldy strings that may be used in more than 1 deployment stage) into a `.env` file and using the CLI to pass in the stage. Go ahead and add a file called `.env` into your project root and set it up as follows.
WEBHOOK_SECRET=<Copy from https://dashboard.stripe.com/test/webhooks>
SECRET_KEY=<Copy from https://dashboard.stripe.com/test/apikeys>
You should now be able to deploy your infrastructure and Lambda function code by running:
STAGE=dev cdk deploy
Your stack should now deploy successfully and print an output similar to this:
The URL described by the outputs can now be added to your Stripe account in the Webhook Settings section.
In the next part of the series we will look at local debugging of your webhook. Subscribe to the news letter to get notified about “Part 3 - Integration testing with the Stripe CLI.”