The Beauty of TypeScript for Scalable Serverless Applications

Arian Acosta
Feb 10 · 5 min read

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.

A theoretical example of a serverless app with too many interconnections

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?

In 2019, JavaScript powers around 66% of AWS Lambdas worldwide(stats).

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.

Lambda using Request/Response Interfaces with TypeScript

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

Drawbacks of Request/Response:

  • It has no runtime validation

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 Promise<GreetingDto>

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

Drawbacks of DTOs:

  • Introduces complexity

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.

Beyond Lambdas

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.

Conclusion

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.

JavaScript in Plain English

Learn the web's most important programming language.

Arian Acosta

Written by

Full-stack developer, cloud enthusiast, and technology explorer. Twitter: @arian3k

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade