typed-serverless
TypeScript icon, indicating that this package has built-in type declarations

0.4.2 • Public • Published

Contributors Forks Stargazers Issues MIT License LinkedIn


Typed Serverless

Fully typed and consistent Serverless Framework configurations, 100% TypeScript, for a world without YAML.
Explore the docs »

View examples · Report Bug · Request Feature

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.

(back to top)

Quick Start

Install Via NPM:

npm install --save-dev typed-serverless typed-aws serverless @serverless/typescript serverless-esbuild ts-node

Create 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 deploy

Usage

resources({ '<ResourceId>': ResourceBuilder })

Used to define your resources.

  • ResourceId should be a valid string literal type defined at Ids in TypedServerless.createDefault<Ids>()

  • ResourceBuilder should be a function like (resourceParams) => SomeResouceObject().

    This function will be called with resourceParams. If your are using TypeServerless.createDefault<>(), it means that your resourceParams will be { name, tags, awsTags }

    • name will be '{service}-{stage}-{ResourceId}'
    • tags will be the same value you define at your serverlessConfig.provider.tags.
    • awsTags will 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'}.

  • ResourceId should be a valid string literal type defined at Ids in TypedServerless.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']}.

  • ResourceId should be a valid string literal type defined at Ids in TypedServerless.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.

  • ResourceId should be a valid string literal type defined at Ids in TypedServerless.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.

  • anyObject should 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.

  • ResourceId should be a valid string literal type defined at Ids in TypedServerless.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!

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

(back to top)

License

Distributed under the MIT License. See LICENSE for more information.

(back to top)

Acknowledgments

(back to top)

Package Sidebar

Install

npm i typed-serverless

Weekly Downloads

1

Version

0.4.2

License

MIT

Unpacked Size

87.5 kB

Total Files

39

Last publish

Collaborators

  • gabrielmoreira