Getting Started With Serverless and TypeScript
If you're doing any "serverless" development on AWS Lambda, you should definitely check out the similarly named Serverless framework. Serverless makes it much easier to build manageable applications with this sort of architecture, taking away a lot of the painful things you would otherwise need to manage and automate yourself.
As you write these event-driven systems, a lot of your functions end up looking like data pipelines - your function receives some input, processes or transforms it as appropriate, and outputs something that might be consumed by a user or even another function. These functional, stateless systems really benefit from having some static type checking, which is where TypeScript can really shine. In this post I'll show how to get started writing a basic Serverless application using TypeScript, and even layer in things like linting and testing that you'd ultimately want in any real application.
Initial Project Setup
If you don't already have Serverless installed, go ahead and do so via NPM:
npm install serverless -g
Once that is installed you will be able to use the serverless
command (or optionally sls
or slss
as shorthand) to invoke different commands for the framework. To start, let's create a new Node app:
sls create -t aws-nodejs
This will create a few files for you:
handler.js
: the function's implementationserverless.yml
: the function's configurationevent.json
: sample function input for local testing
This is obviously a JavaScript implementation, so now we can start replacing it with TypeScript. First, go ahead and install a few NPM packages:
npm i --save-dev typescript webpack ts-loader serverless-webpack
If you're familiar with using Webpack on other platforms, such as the web where it's most common, then this should feel pretty familiar. Serverless provides a nice extensibility model, which helps enable us to leverage the same Webpack model we use elsewhere. Next, create a webpack.config.js
file to configure Webpack:
var path = require('path');
module.exports = {
entry: './handler.ts',
target: 'node',
module: {
loaders: [
{ test: /\.ts(x?)$/, loader: 'ts-loader' }
]
},
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx', '']
},
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, '.webpack'),
filename: 'handler.js'
},
};
This is boilerplate Webpack, passing TypeScript files through the TypeScript compiler and outputting a JavaScript file. We can also add a tsconfig.json
file to configure TypeScript:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs"
},
"exclude": [
"node_modules"
]
}
With that ready to go, let's update serverless.yml
:
service: serverless-typescript-demo
provider:
name: aws
runtime: nodejs4.3
plugins:
- serverless-webpack
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
There are a few things going on in here:
service
defines the name for this serviceplugins
defines the plugins we want to use, which in our case is the Webpack pluginfunctions
lists out all functions for this servicehello
is defined as a function, specifies its handler method, and also connects aGET /hello
HTTP endpoint to the function
With the plugin added, running the serverless
command will now show you a few new commands added by the Webpack plugin. Before we do that, the last thing we need to do is create the TypeScript version of the handler. Rename handler.js
to handler.ts
, and replace the contents with:
export function hello(event, context, callback) {
callback(null, {
message: 'Hello from TypeScript!',
event
});
};
The function's handler receives three parameters here:
event
: the payload for the event that triggered the functioncontext
: information about the context in which the function is runningcallback
: a callback that must be invoked by the function to say that the function has completed, and also specify success or failure
In this case we simply return a successful invocation, where the response payload contains a message
string and also relays the event that it received.
You can go ahead and run this locally by running:
sls webpack invoke -f hello -p event.json
Go ahead and replace the default contents of event.json
with something that matches the API Gateway event format:
{
"method": "GET",
"headers": {
"host": "localhost:8000",
"user-agent": "curl/7.49.1",
"accept": "*/*"
},
"body": {},
"path": {},
"query": {
"foo": "bar"
}
}
You can also simulate API Gateway locally by running:
sls webpack serve
This will allow you to hit http://localhost:8000/hello
to trigger your function. If you run sls deploy
Serverless will deploy your function out to AWS and provide you with the endpoint that you can hit there as well.
Add Some Types
That's all well and good, but technically everything there was untyped. Let's go ahead and add some basic types here in a new file named models.ts
:
export interface IResponsePayload {
message: string;
event: any;
}
export interface IQueryParameters {
foo: string;
}
export interface IEventPayload {
method: string;
query: IQueryParameters;
}
export interface ICallback {
(error: any, result: IResponsePayload): void;
}
Now we have some really basic types around the function's input and output, so let's update the handler to take advantage of that:
import { ICallback, IEventPayload } from './models';
export function hello(event: IEventPayload, context, callback: ICallback) {
callback(undefined, {
message: `Method: ${event.method}, Param: ${event.query.foo}`,
event: event
});
}
If you go ahead and invoke the function again you should see basically the same output as before, except that the message
will reflect what you provide via the query string ?foo=
. If you tweak the format string for message
to try and use event.query.bar
, you'll see the TypeScript compile step fail, just the way we want. These are obviously trivial types, but as you start to build out larger systems that deal with real events and data, these types can save you from a lot of mistakes that are otherwise easy to make.
Add Linting
If you're like me, you like to make sure you have some linting rules set up for all your JavaScript and TypeScript projects. In the spirit of setting this up like a real world project, let's layer that in. For this example I'll use the TSLint config we use at Olo:
npm i --save-dev tslint tslint-config-olo
We can add a tslint.json
file to specify we'd like to use this config:
{
"extends": "tslint-config-olo"
}
Finally, we'll add an NPM command named lint
in package.json
to run TSLint:
"scripts": {
"lint": "tslint *.ts"
},
Now you can run npm run lint
to lint all the TypeScript files in the current folder.
Add Tests
Finally, no application is complete without some tests, so let's set some up. You're welcome to use your favorite testing frameworks, of course, but for this example I'm going to use Karma, Mocha, and Chai:
npm i --save-dev mocha chai karma karma-typescript-preprocessor2 karma-webpack karma-chai karma-mocha phantomjs-prebuilt karma-phantomjs-launcher
Next, go ahead and create karma.conf.js
which will configure the test runner and Webpack:
var webpackConfig = require('./webpack.config');
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['mocha', 'chai'],
files: ['tests.ts'],
preprocessors: {
'tests.ts': ['webpack']
},
webpack: {
module: webpackConfig.module,
resolve: webpackConfig.resolve
},
reporters: ['progress'],
colors: true,
logLevel: config.LOG_INFO,
browsers: ['PhantomJS'],
singleRun: true
});
}
We can also install some typings for Mocha and Chai:
typings i --save dt~mocha --global
typings i --save chai
Next, let's add another NPM command to run the tests:
"scripts": {
"test": "karma start"
},
Finally, we can add a basic test file named tests.ts
:
import { hello } from './handler';
import * as chai from 'chai';
const expect = chai.expect;
describe('hello function', () => {
it('processes the query string', done => {
const requestEvent = {
method: 'GET',
query: {
foo: 'bar'
}
};
hello(requestEvent, {}, (err, result) => {
expect(err).to.be.undefined;
expect(result.event).to.equal(requestEvent);
expect(result.message).to.equal('Method: GET, Param: bar');
done();
});
});
});
Now if you run npm run test
you should see one successful test.
Summary
This only scratches the surface of what you can do with Serverless, especially when paired with TypeScript, but hopefully you can start to see the potential here. Admittedly the linting and testing pieces are somewhat tangential here, but I really wanted to show how easy it is to get started building out a real-world application on top of Serverless while being able to leverage modern technologies like TypeScript and Webpack. The full source for this example is available on GitHub.