Serverless is the new kid in the block. It seems to be a magical solution for all scalability problems while keeping costs low. As we venture into this realm we quickly realize that developing and maintaining large applications might be not as simple as it seems. Actually, it’s quite hard to get it right. In this post, we will explore using TypeScript interfaces to set contracts across serverless resources.
Decoupling is great! Until it is not…
One of the key concepts of serverless architectures is to have small functional pieces that are decoupled — as in completely separate from each other. Each one of those pieces will scale at different rates providing optimal usage/cost balance. So for instance in an e-commerce web application, a Lambda responsible for authentication could be invoked many more times than another responsible for processing credit card transactions.
Naturally, these functional pieces need to communicate with each other sending data back and forth through synchronous or asynchronous channels such as queues, step functions, notifications, etc. One can quickly see that as the application grows, the number of connections between parts increases at an exponential rate.
If you have infrastructure that is tangled like this, then it is very easy to make unintentional breaking changes because it’s difficult to track what invokes what. Making a simple change the input format of a Lambda could potentially take down an entire portion of the system if some of the callers are not updated accordingly.
What does TypeScript have to do with all this?
TypeScript provides a unique mechanism for declaring interfaces and types that are easy to apply, extend and share across multiple codebases. Using TypeScript will decrease the development time and virtually eliminate communication format errors because you will know exactly what to send and what to expect from a Lambda function even if it was created by another team. Maintain your applications with confidence by embracing an infrastructure-as-code approach in which serverless resources are considered just another piece of code that has a defined input and output.
There are many ways to take advantage of TypeScript, in this post we will explore a couple of approaches.
The Request/Response Pattern
This is the most basic pattern, and it is useful for when you have a Lambda that expects a certain input, performs some processing and outputs the results. Although this one is simple, it can prevent many headaches.
Now when invoking this function you know that it is expecting an object that has the shape of
GetGreetingRequest and that the returned object will have the properties defined by
GetGreetingResponse. Note that both interfaces are exported, so that we can use them where you are invoking the Lambda:
Here we have completed the most basic cycle. The consumer uses the interfaces to make sure that the input and output are what’s expected. If later the Lambda changes along with its interfaces, then this code would not compile preventing us from using an outdated format.
Advantages of Request/Response:
- Provides type checking and autocompletion
- It’s easy to implement
Drawbacks of Request/Response:
- It has no runtime validation
- Forces assertion with
Payload as string
- Business logic is mixed with data representation
The DTO Pattern
DTOs or Data Transfer Objects have been around for a while, but in the modern serverless world, they are particularly relevant. They are useful to pass data across resources, for instance, Lambdas invoking other Lambdas, or dispatching messages to an SQS queue to be processed later on.
A Data Transfer Object’s main purpose is to provide a mechanism for reliably transfering data by extracting the logic of packing and unpacking data from the sender and the receiver.
In the following example, we are going to use two lambdas. The idea is that the first lambda sends a
UserDto with the user information and the second lambda will respond with a
GreetingDto containing the message.
Sending a DTO:
Take a closer look at the
userDto . It’s an instance of
UserDto and it contains all the properties of the user. Most importantly it has a
serialize method that abstracts the logic of stringifying the data for transmission.
Receiving a DTO:
Once the Greeter lambda is invoked it receives an object, but we don’t really know what type it is. It’s possible that it was invoked with an incorrect input. For that reason, we need to verify at run time that it is actually compatible with
UserDto. Thankfully, our class has a static method
from that validates it and returns an instance.
On the Greeter side, we get the event, and with a single line, we validate and get the user instance. Also for extra safety, it declares the return type as
Here, AWS processes the returned instance of
GreetingDto and returns an object with its properties. Note that it’s not necessary to stringify the return.
Advantages of DTOs:
- Validates input at run time and provides type checking
- Abstracts data transmission logic and makes it reusable
- Simplifies communication with other systems
Drawbacks of DTOs:
- Introduces complexity
- For intricate object schemas, a validator package like
yupmight be useful
Syncing across Teams and Projects
One of the features I like most about TypeScript is the ability to export type declarations by themselves. This in conjunction with NPM packages is particularly powerful when there are multiple teams working in an organization.
By having a centralized repository, each team can include those packages in their projects to send and receive data in a predictable way. If using the Request/Response pattern, you will only need to export the TypeScript interfaces, but if you want to go a step further, you can also provide the implementation of the DTOs along with the type declarations for usage with TS and JS.
The above patterns are also useful for other serverless resources such as SQS, Step Functions, DynamoDB, and the newly added CDK. All of these resources benefit from a predictable object structure that you can pass around reliably. For instance, SQS can invoke Lambdas with an array of messages, and each of those messages can be type-checked.
With the increased popularity of serverless functions and other resources, it has become difficult to keep track of changes and ensuring that everything works as it should. TypeScript provides great functionality for type-checking that can be distributed and applied in many ways. It’s a powerful tool that could be key to the success of your serverless application.