Create a Serverless API endpoint
To setup a new serverless API endpoint function in this repo, follow these steps:
Install Serverless Framework CLI
- To install Serverless Framework CLI, run the following:
npm i -g serverless
Directory Setup
- Create a new folder with the name of the service inside the servicesdirectory- e.g: If your service name is email, then create the new directoryservices/email.
 
- e.g: If your service name is 
- Run the following command and follow the prompt:
npm init --scope @acmutd -w ./services/email
info
- When run the above command, or any command afterward, make sure to replace emailwith the name of your service.
- Run Monorepo: sync workplace featuresin VSCode command palette.
Install Serverless Function Dependencies
- To install the dependencies that will be used by Serverless Framework, run the following command:
npm i -w @acmutd/email -D serverless-iam-roles-per-function serverless-create-global-dynamodb-table serverless-offline serverless-prune-plugin
- To install AWS Lambda related dependencies, run the following command:
npm i -w @acmutd/email aws-sdk aws-lambda
- In case root package.jsondoes not have powertools, run the following command to install them:
npm i @dazn/lambda-powertools-cloudwatchevents-client @dazn/lambda-powertools-correlation-ids @dazn/lambda-powertools-logger @dazn/lambda-powertools-pattern-basic @dazn/lambda-powertools-lambda-client @dazn/lambda-powertools-sns-client @dazn/lambda-powertools-sqs-client @dazn/lambda-powertools-dynamodb-client @dazn/lambda-powertools-kinesis-client
Setup Linting
- To install linting dependencies, run the following command:
npm i -D -w @acmutd/email eslint eslint-config-airbnb-base typescript-eslint eslint-plugin-import eslint-import-resolver-alias eslint-plugin-module-resolver @typescript-eslint/eslint-plugin @typescript-eslint/parser
- Create a file named .eslintrc.jsonin your service directory with the following configurations:
{
  "extends": ["../../.eslintrc.json"],
  "parserOptions": {
    "tsconfigRootDir": ".",
    "project": "./tsconfig(.*)?.json"
  },
  "env": {
    "node": true
  },
  "root": true,
  "rules": {
    "@typescript-eslint/no-unsafe-assignment": "off",
    "@typescript-eslint/no-unsafe-member-access": "off",
    "@typescript-eslint/no-var-requires": "off",
    "global-require": "off",
    "react/no-array-index-key": "off",
    "react/jsx-props-no-spreading": "off"
  },
  "settings": {
    "import/resolve": {
      "typescript": {
        "alwaysTryType": true,
        "paths": "./tsconfig.json"
      },
      "alias": {
        "map": [
          ["@src", "./src"],
          ["@tests", "./tests"]
        ],
        "extensions": [".ts"]
      }
    }
  }
}
- Create a file named .eslintignorewith the following configuration:
node_modules
.build
dist
generated
*.config.*
- Create a file named .lintstagedrc.jsonwith the following configuration:
{ "**/*.{js,ts,tsx}": ["eslint --fix", "prettier --write '**/*.{js,ts,tsx}'"] }
Setup Jest for Testing
- To install Jest dependencies, run the following command:
npm i -w @acmutd/email -D jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript
- Create a file named babel.config.jswith the following configuration:
module.exports = {
  presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
};
Configure Scripts
- Configure the scriptsobject in thepackage.jsonfile as follow:
"scripts": {
    "test": "NODE_ENV=test ../../node_modules/.bin/jest --ci --verbose",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "buildtest": "tsc --noEmit",
    "sls:offline": "sls offline --httpPort='5000'"
},
info
If your service is dependent on Doppler environment variable, the command for sls:offline should be the following:
doppler run -- sls offline
Configure Webpack
- To install dependencies for Webpack, run the following command:
npm i -w @acmutd/email -D fork-ts-checker-webpack-plugin webpack-node-externals serverless-webpack
- Create a file named webpack.config.jswith the following configurations:
const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
  context: __dirname,
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: slsw.lib.webpack.isLocal ? 'eval-cheap-module-source-map' : 'source-map',
  resolve: {
    extensions: ['.mjs', '.js', '.json', '.ts'],
    // symlinks: false,
    // cacheWithContext: false,
    alias: {
      '@src': path.resolve(__dirname, './src'),
      '@tests': path.resolve(__dirname, './tests'),
    },
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  // experiments: {
  //   topLevelAwait: true,
  // },
  target: 'node',
  externalsPresets: { node: true },
  externals: [nodeExternals()],
  optimization: {
    minimize: true,
  },
  module: {
    rules: [
      {
        test: /\.(tsx?)$/,
        loader: 'babel-loader',
        exclude: [
          [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, '.serverless'),
            path.resolve(__dirname, '.webpack'),
          ],
        ],
        options: {
          presets: [
            ['@babel/preset-env', { targets: { node: 'current' } }],
            '@babel/preset-typescript',
          ],
          plugins: [
            'babel-plugin-transform-typescript-metadata',
            ['@babel/plugin-proposal-decorators', { legacy: true }],
            ['@babel/plugin-proposal-class-properties', { loose: true }],
            'babel-plugin-parameter-decorator',
            ['@babel/plugin-proposal-private-methods', { loose: true }],
            ['@babel/plugin-proposal-private-property-in-object', { loose: true }],
          ],
        },
      },
    ],
  },
  plugins: [new ForkTsCheckerWebpackPlugin()],
};
Setup Typescript
- Create a tsconfig.jsonfile with the following configuration:
{
  "compilerOptions": {
    "strict": true,
    // project options
    "lib": ["ES2020"], // specifies which default set of type definitions to use ("DOM", "ES6", etc)
    "outDir": "lib", // .js (as well as .d.ts, .js.map, etc.) files will be emitted into this directory.,
    "removeComments": true, // Strips all comments from TypeScript files when converting into JavaScript- you rarely read compiled code so this saves space
    "target": "ES2020", // Target environment. Most modern browsers support ES6, but you may want to set it to newer or older. (defaults to ES3)
    "module": "ESNext",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"],
    // Module resolution
    "baseUrl": "./", // Lets you set a base directory to resolve non-absolute module names.
    "esModuleInterop": true, // fixes some issues TS originally had with the ES6 spec where TypeScript treats CommonJS/AMD/UMD modules similar to ES6 module
    "moduleResolution": "node", // Pretty much always node for modern JS. Other option is "classic"
    "paths": {
      "@src/*": ["src/*"],
      "@tests/*": ["tests/*"]
    }, // A series of entries which re-map imports to lookup locations relative to the baseUrl
    // Source Map
    "sourceMap": true, // enables the use of source maps for debuggers and error reporting etc
    "sourceRoot": "/", // Specify the location where a debugger should locate TypeScript files instead of relative source locations.
    // Strict Checks
    "alwaysStrict": true, // Ensures that your files are parsed in the ECMAScript strict mode, and emit “use strict” for each source file.
    "allowUnreachableCode": false, // pick up dead code paths
    "noImplicitAny": true, // In some cases where no type annotations are present, TypeScript will fall back to a type of any for a variable when it cannot infer the type.
    "strictNullChecks": true, // When strictNullChecks is true, null and undefined have their own distinct types and you’ll get a type error if you try to use them where a concrete value is expected.
    // Linter Checks
    // "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true, // accessing index must always check for undefined
    // "noUnusedLocals": true, // Report errors on unused local variables.
    // "noUnusedParameters": true, // Report errors on unused parameters in functions
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "allowSyntheticDefaultImports": true
  },
  "typeRoots": ["./node_modules/@types", "../../node_modules/@types"],
  "include": ["./**/*.ts"],
  "exclude": [
    "node_modules/**/*",
    ".serverless/**/*",
    ".webpack/**/*",
    "_warmup/**/*",
    ".vscode/**/*",
    "*.config.js"
  ],
  "extends": "../../tsconfig.json"
}
Configure Serverless YML file
- Create the serverless.ymlfile with the following configuration:
service: email
custom:
  webpack:
    webpackConfig: webpack.config.js
    includeModules:
      forceExclude:
        - aws-sdk
    packager: npm
    packagerOptions:
      lockFile: '../../package-lock.json'
  prune:
    automatic: true
    number: 3
plugins:
  - serverless-offline
  - serverless-webpack
  - serverless-iam-roles-per-function
  - serverless-create-global-dynamodb-table
  - serverless-prune-plugin
provider:
  name: aws
  region: us-east-1
  runtime: nodejs14.x
  memorySize: 256
  httpApi:
    payload: '2.0'
    cors: true
Create a Serverless function
- Create file src/handler.ts, in which will contain the serverless function.
- An example of the src/handler.tswill be as follow:
export const sendEmailHandler = async (event: APIGatewayProxyEvent) => {
  const eventBody = JSON.parse(event.body || '{}') as SendEmailConfig;
  if (!validateRequest(eventBody)) {
    return {
      msg: 'Missing fields',
    };
  }
  const sendEmailStatus = await sendEmail(eventBody);
  return {
    msg: sendEmailStatus ? 'Successfully sent email' : 'Unsucessfully sent email',
  };
};
- Add the following into serverless.yml:
functions:
  sendgrid:
    handler: src/handler.sendEmailHandler
    events:
      - httpApi:
          # This is endpoint for function
          path: /email
          # This is HTTP method
          method: post
Run the function locally
- To run an emulator of the function run the following command from service directory:
npm run sls:offline
- Another way to run the service would be the following command from the root dir:
npx turbo run sls:offline --scope="@acmutd/email"
caution
The above command can only be run from the root directory.
- The function should now be accesible at http://localhost:5000/email
note
- emailshould be replace with your path name specified in the- serverless.ymlfile.