typed-serverless v0.4.2
Contents
Why?
TL;DR YAML should not exist. https://noyaml.com/
Typed Serverless helps you build a Serverless application using only TypeScript & Serverless Framework. It's a small library to help you build strongly typed serverless.ts, including AWS CloudFormation resources, logical ids and references between resources, all typed and with an excellent autocomplete.
Serverless Framework is a great framework to create serverless applications, but usually you will see YAML configurations instead of TypeScript. Usually it starts with a very simple serverless.yaml configuration, but as more features are added, it becomes tricky to ensure that everthing is correctly configured and consistent. In some cases YAML is not enough, and we need some serverless plugin to rescue us.
Some reasons:
Save time. Many errors that would only happen at runtime can be prevented by using TypeScript. For example, a very common mistake is referencing a wrong ARN resource when defining an IAM Policy.
Autocomplete for almost everything, including all IDs you define for your resources, and also all AWS CloudFormation resources using Typed AWS.
Ensure a consistent style. Avoid hundreds of string interpolations and copy-and-paste by creating helper functions.
It reduces the need for some Serverless plugins for simple tasks like conditionals, resource tags, environment validation, and anything else that can be replaced by an imperative programming language.
Quick Start
Install Via NPM:
npm install --save-dev typed-serverless typed-aws serverless @serverless/typescript serverless-esbuild ts-nodeCreate a Serverless configuration
Create a serverless.ts:
import type { AWS } from '@serverless/typescript';
import { TypedServerless, SQS } from 'typed-serverless';
type Ids = 'MyQueue' | 'SendMessageFn';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
service: 'minimal',
plugins: ['serverless-esbuild'],
provider: {
name: 'aws',
runtime: 'nodejs14.x',
region: 'eu-west-1',
lambdaHashingVersion: '20201221',
tags: {
myCustomTag: 'my-sample-tag',
},
iam: {
role: {
statements: [
{
Effect: 'Allow',
Action: 'sqs:*',
Resource: typed.getArn('MyQueue'),
},
],
},
},
},
resources: {
Resources: {
...typed.resources({
'MyQueue': ({ name, awsTags }) =>
SQS.Queue({ QueueName: name, Tags: awsTags}),
}),
},
},
functions: typed.functions({
'SendMessageFn': ({ name }) => ({
name,
handler: './mylambda.handler',
events: [{ http: { method: 'get', path: 'send' } }],
environment: {
QUEUE_URL: typed.ref('MyQueue'),
},
}),
}),
};
module.exports = typed.build(serverlessConfiguration);Create your Lambda
Create your first lambda file mylambda.handler:
import { SQS } from 'aws-sdk';
const sqs = new SQS();
export const handler = async (event) => {
const messageSent = await sqs
.sendMessage({
QueueUrl: process.env.QUEUE_URL,
MessageBody: JSON.stringify({
createdAt: new Date().toISOString(),
event,
}),
})
.promise();
return { statusCode: 200, body: JSON.stringify({ messageSent })};
};Deploy it!
npx sls --stage dev deployUsage
resources({ '<ResourceId>': ResourceBuilder })
Used to define your resources.
ResourceIdshould be a valid string literal type defined atIdsinTypedServerless.createDefault<Ids>()ResourceBuildershould be a function like(resourceParams) => SomeResouceObject().This function will be called with
resourceParams. If your are usingTypeServerless.createDefault<>(), it means that yourresourceParamswill be{ name, tags, awsTags }namewill be'{service}-{stage}-{ResourceId}'tagswill be the same value you define at your serverlessConfig.provider.tags.awsTagswill be your serverlessConfig.provider.tags converted to an array of{ Key: string, Value: string }
Important: Always use name in your resource, so this way, Typed Serverless can keep track of any logical id to name mapping. It's important for other features like .getName('ResourceId').
Tip: We are using typed-aws to provide almost all CloudFormation Resource. Please check if your resource is available.
Tip: You can also create your own resourceParams by providing a custom resourceParamsFactory through TypedServerless.create<Ids>({ resourceParamsFactory }). See our default implementation at https://github.com/gabrielmoreira/typed-serverless/tree/main/src/aws/defaults.ts
E.g.
type Ids = 'MyQueue' | 'SomeOtherResource';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({ QueueName: name }), // Always use the provided name
'SomeOtherResource': ...
})
...
} ref('<ResourceId>') or getRef('<ResourceId>')
Use this function to make a CloudFormation reference to a resource. E.g. {'Ref': 'SomeResourceId'}.
ResourceIdshould be a valid string literal type defined atIdsinTypedServerless.createDefault<Ids>()
Important: Every CloudFormation resource has a differente return value for a Ref property. E.g. SQS Queues Ref property returns the queue URL.
Note: This function will also validate if you are referencing a defined resource using resources({ ... })
type Ids = 'MyQueue' | 'MyDeadLetterQueue';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
resources: {
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({ QueueName: name }),
}),
},
functions: typed.functions({
'SendMessageFn': ({ name }) => ({
name,
handler: './mylambda.handler',
events: [{ http: { method: 'get', path: 'send' } }],
environment: {
QUEUE_URL: typed.ref('MyQueue'),
},
}),
}),
...
} arn('<ResourceId>') or getArn('<ResourceId>')
Use this function to make a CloudFormation reference to a resource ARN. E.g. {'Fn::GetAtt': ['SomeResourceId', 'Arn']}.
ResourceIdshould be a valid string literal type defined atIdsinTypedServerless.createDefault<Ids>()
Note: This function will also validate if you are referencing a defined resource using resources({ ... })
type Ids = 'MyQueue' | 'MyDeadLetterQueue';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({
QueueName: name,
RedrivePolicy: {
deadLetterTargetArn: typed.arn('MyDeadLetterQueue'),
maxReceiveCount: 3,
},
}),
'MyDeadLetterQueue': ({ name }) =>
SQS.Queue({ QueueName: name }),
})
...
} getName('<ResourceId>')
Use this function to get a resource name.
ResourceIdshould be a valid string literal type defined atIdsinTypedServerless.createDefault<Ids>()
Important:
Note: This function will also validate if you are referencing a defined resource using resources({ ... })
type Ids = 'MyQueue' | 'MyDeadLetterQueue';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({
QueueName: name,
RedrivePolicy: {
deadLetterTargetArn: typed.arn('MyDeadLetterQueue'),
maxReceiveCount: 3,
},
}),
'MyDeadLetterQueue': ({ name }) =>
SQS.Queue({ QueueName: name }),
})
...
} stringify( anyObject )
Use this function to be able to serialize an object to a JSON string when you also want to support CloudFormation expressions evaluation inside the object.
The main use case for this is to overcome a limitation in CloudFormation that does not allow using CloudFormation intrinsic functions (like Fn::Get, Ref, Fn::*) in a JSON string. This is common when creating a AWS CloudWatch Dashboard, a Step Function State Machine, and other places.
anyObjectshould be any valid TypeScript object.
type Ids = 'MyQueue' | 'MyDeadLetterQueue';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
resources: {
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({ QueueName: name }),
}),
},
functions: typed.functions({
'SendMessageFn': ({ name }) => ({
name,
handler: './mylambda.handler',
events: [{ http: { method: 'get', path: 'send' } }],
environment: {
COMPLEX_JSON_STRING: typed.stringify({
// typed.stringify will preserve all CloudFormation expressions (.ref, .arn, .getName) below:
queueUrl: typed.ref('MyQueue'),
queueArn: typed.arn('MyQueue'),
queueName: typed.getName('MyQueue'),
})
},
}),
}),
...
} buildLambdaArn('<ResourceId>')
Use this function to be able to make a soft reference to a lambda. It means that instead of creating a CloudFormation reference like .arn('<ResourceId>'), we build an ARN string for your resource, be we also validate if this resource was previously defined using .resources({'<ResourceId>': <ResourceBuilder> })
The main use case for this is to overcome a limitation in CloudFormation that does not allow a circular reference. It's very common issue when you have a lambda that references an IAM policy that references to the same lambda, creating a circular dependency.
ResourceIdshould be a valid string literal type defined atIdsinTypedServerless.createDefault<Ids>()
type Ids = 'MyQueue' | 'MyDeadLetterQueue';
const typed = TypedServerless.createDefault<Ids>();
const serverlessConfiguration: AWS = {
...
resources: {
Resources: typed.resources({
'MyQueue': ({ name }) =>
SQS.Queue({ QueueName: name }),
}),
},
functions: typed.functions({
'SendMessageFn': ({ name }) => ({
name,
handler: './mylambda.handler',
events: [{ http: { method: 'get', path: 'send' } }],
environment: {
COMPLEX_JSON_STRING: typed.stringify({
// typed.stringify will preserve all CloudFormation expressions (.ref, .arn, .getName) below:
queueUrl: typed.ref('MyQueue'),
queueArn: typed.arn('MyQueue'),
queueName: typed.getName('MyQueue'),
})
},
}),
}),
...
} TypedServerless.create(param)
TODO Document
resourceParamsFactory: (id: TId, config: TConfigType) => TResourceParams;
onResourceCreated?: (resource: Resource<CfnResourceProps>) => void;
onFunctionCreated?: (lambda: ServerlessFunction) => void;TODO Document
extendWith((typed) => extension)
TODO Document
only<OtherIds>(object)
TODO Document
Examples
Check out our examples.
Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
License
Distributed under the MIT License. See LICENSE for more information.