📢 TypeScript Project Setup for AWS Lambda and Build Process

This setup provides a streamlined approach to code sharing and compilation across services in our backend project, ensuring a consistent and efficient development workflow. Here’s a summary of the setup and the build process.

:file_folder: Folder Structure

The root project structure is organized as follows:

Project Folder:
    entities/
         Admin.ts
    lib/
        helper.ts
        aws/
            ses.ts
    services/
        admins/
            src/
                  handles/
                       handler.ts
                  utils/
                       util.ts

Each service (like admins) has a dedicated folder with its own tsconfig.json for TypeScript configuration.

:gear: TypeScript Configuration (tsconfig.json)

{
  "compilerOptions": {
    "outDir": "./dist", // Root output directory
    "module": "CommonJS",
    "target": "ES2020",
    "strict": true,
    "rootDir": "./src",
    "baseUrl": "./",
    "paths": {
      "@lib/*": ["../../lib/src/*"],
      "@handlers/*": ["src/handlers/*"],
      "@utils/*": ["src/utils/*"]
    },
    "skipLibCheck": true // Skip type checking for `lib`
  },
  "include": ["src/handlers/**/*.ts", "src/utils/**/*.ts", "src/lib/**/*.ts", "src/entities/*.ts"],
  "exclude": ["node_modules"]
}

This configuration ensures:

  • Compilation output goes to ./dist.
  • Alias paths simplify imports like @lib/*.
  • Strict type-checking enhances code quality.

:rocket: Automated Build Process with Serverless Framework

For each service, the serverless.yml file is updated with custom hooks to automate TypeScript compilation during the following stages:

  1. Before packaging for deployment (sls deploy).
  2. Before running Serverless Offline (sls offline).

Here’s the relevant section (serverless.yml):

# Include and Exclude Packages during Deployment
package:
  patterns:
    - dist/**             # Include only compiled JS files
    - "!node_modules/**"  # Exclude dependencies
    - "!lib/**"           # Exclude raw TypeScript files in lib
    - "!entities/**"      # Exclude raw TypeScript files in entities
    - "!.git/**"          # Exclude Git metadata
    - "!docs/**"          # Exclude documentation
    - "!**/*.ts"          # Exclude all TypeScript files
    - "!tsconfig.json"    # Exclude tsconfig.json

custom:
  # Scripts to build project when deploying and running SLS Offline
  scripts:
    hooks:
      # Run before the packaging process
      'package:initialize': |
        # Navigate to the root directory and run ts-build.sh
        echo "Building service before deployment - Running ts-build.sh"
        cd ../../ || { echo "Failed to navigate to the project root"; exit 1; }
        echo "Current directory: $(pwd)"

        # Run the ts-build.sh script
        chmod +x ts-build.sh || { echo "Failed to make ts-build.sh executable"; exit 1; }
        ./ts-build.sh ${self:service} || { echo "Build for ${self:service} failed"; exit 1; }
        echo "Build completed for ${self:service}"

      # Run before invoking serverless offline
      'before:offline:start': |
        # Navigate to the root directory and run ts-build.sh
        echo "Building service before deployment - Running ts-build.sh"
        cd ../../ || { echo "Failed to navigate to the project root"; exit 1; }
        echo "Current directory: $(pwd)"
        
        # Run the ts-build.sh script
        chmod +x ts-build.sh || { echo "Failed to make ts-build.sh executable"; exit 1; }
        ./ts-build.sh ${self:service} || { echo "Build for ${self:service} failed"; exit 1; }
        echo "Build completed for ${self:service}"

plugins:
  - serverless-offline
  - serverless-domain-manager
  # Plugin to run the scripts
  - serverless-plugin-scripts

This configuration ensures that only the necessary compiled files are included in the deployment package, while excluding unnecessary raw TypeScript files (as lambda will have compiled version of javascript files in /dist folder and it doesn’t need typescript files), documentation, and metadata. Additionally, the Serverless Offline and Serverless Domain Manager plugins are used for local development and custom domain handling, while the Serverless Plugin Scripts plugin helps automate specific tasks during the build process.

:scroll: Build Script: ts-build.sh

The build script performs the following actions:

  1. Copies shared resources (lib/, entities/) into each service.
  2. Runs the TypeScript compiler (tsc) for the specific service.
  3. Cleans up copied resources after successful compilation.

Key features:

  • Flexibility to build a specific service or all services at once.
  • Error handling to ensure a smooth build process.

Example:

#!/bin/bash

# Build a specific service
build_service() {
  local service=$1
  local service_dir="services/$service"
  local tsconfig="$service_dir/tsconfig.json"

  # Check if tsconfig.json exists
  if [ ! -f "$tsconfig" ]; then
    echo "Skipping $service (no tsconfig.json found)"
    return
  fi

  # Define the source folder and destination name
  SOURCE_FOLDER_LIB="lib"
  SOURCE_FOLDER_ENTITIES="entities"
  DESTINATION_NAME="$service_dir/src"

  # Check if the source folder exists
  if [ ! -d "$SOURCE_FOLDER_LIB" ]; then
    echo "Source folder '$SOURCE_FOLDER_LIB' does not exist. Exiting."
    exit 1
  fi

  # Check if the source folder exists
  if [ ! -d "$SOURCE_FOLDER_ENTITIES" ]; then
    echo "Source folder '$SOURCE_FOLDER_ENTITIES' does not exist. Exiting."
    exit 1
  fi
  
  # Copy the source folders into the service directory
  echo "Copying source folders..."
  echo "Copying from: $SOURCE_FOLDER_LIB -> $DESTINATION_NAME"
  cp -r "$SOURCE_FOLDER_LIB" "$DESTINATION_NAME"
  echo "Copying from: $DESTINATION_NAME -> $SOURCE_FOLDER_ENTITIES"
  cp -r "$SOURCE_FOLDER_ENTITIES" "$DESTINATION_NAME"

  # Navigate to the service directory and build
  echo "Building $service..."
  (cd "$service_dir" && tsc --project tsconfig.json)
  if [ $? -eq 0 ]; then
    echo "Build successful for $service."
  else
    echo "Build failed for $service."
  fi

  # Delete the copied folders
  echo "Cleaning up..."
  echo "Removing folder: $DESTINATION_NAME/$SOURCE_FOLDER_LIB"
  rm -rf "$DESTINATION_NAME/$SOURCE_FOLDER_LIB"
  echo "Removing folder: $DESTINATION_NAME/$SOURCE_FOLDER_ENTITIES"
  rm -rf "$DESTINATION_NAME/$SOURCE_FOLDER_ENTITIES"
  echo "Cleanup complete. Removed the copied folders."

}

build_resources() {
  local resources_dir="./resources/"
  local tsconfig="./tsconfig.json" # Adjusted path to the resources tsconfig.json

  # Check if tsconfig.json exists
  if [ ! -f "$tsconfig" ]; then
    echo "Skipping resources (no tsconfig.json found)"
    return
  fi

  # Navigate to the resources directory and build
  echo "Building resources..."
  (cd "$resources_dir" && tsc --project "$tsconfig")
  if [ $? -eq 0 ]; then
    echo "Build successful for resources."
  else
    echo "Build failed for resources."
  fi
}

build_resources() {
  local resources_dir="./resources/"
  local tsconfig="./tsconfig.json"  # Adjusted path to the resources tsconfig.json

  # Check if tsconfig.json exists
  if [ ! -f "$tsconfig" ]; then
    echo "Skipping resources (no tsconfig.json found)"
    return
  fi

  # Navigate to the resources directory and build
  echo "Building resources..."
  (cd "$resources_dir" && tsc --project "$tsconfig")
  if [ $? -eq 0 ]; then
    echo "Build successful for resources."
  else
    echo "Build failed for resources."
  fi
}

# Iterate through all services
build_all_services() {
  for service in services/*; do
    if [ -d "$service" ]; then
      build_service "$(basename "$service")"
    fi
  done
}

# Main logic
if [ "$#" -gt 0 ]; then
  # Build specific services, lib, or resources if arguments are provided
  for arg in "$@"; do
    if [ "$arg" == "resources" ]; then
      build_resources
    elif [ -d "services/$arg" ]; then
      build_service "$arg"
    else
      echo "Service or resource '$arg' does not exist."
    fi
  done
else
  echo "No arguments provided. Building all services and resources."
  # Default: Build all services and resources
  build_resources
  build_all_services
fi

echo "Build process completed."

:gear: ESLint Configuration (eslint.config.js)

Below is the configuration structure, designed to support both TypeScript and JavaScript while ignoring irrelevant files like node_modules and dist:

Key Highlights:

  1. Separate Rules for TypeScript and JavaScript:
  • TypeScript files (.ts, .tsx) get special handling with the @typescript-eslint plugin and parser.
  • JavaScript files (.js) follow best practices with standard ESLint rules.
  1. CommonJS and ES Modules Support:
  • The setup works seamlessly with both require-based and import/export-based modules.
  1. Customizable Rules:
  • For example, no console.log is allowed, but console.warn and console.error are exceptions.
  • Tabs are replaced with 4 spaces for consistent formatting.

eslint.config.js

const js = require('@eslint/js')
const tsParser = require('@typescript-eslint/parser') // Import tsParser
const tsPlugin = require('@typescript-eslint/eslint-plugin')

module.exports = [
    {
        // Ignore node_modules and dist folders
        ignores: ['node_modules/**', '**/dist/**'],
    },
    {
        // Special configuration for ESLint config files (eslint.config.js)
        files: ['eslint.config.js'],
        languageOptions: {
            ecmaVersion: 2021,
            sourceType: 'script', // CommonJS modules
            globals: {
                module: 'readonly',
                __dirname: 'readonly',
                require: 'readonly',
            },
        },
    },
    {
        // Apply to TypeScript files
        files: ['**/*.ts', '**/*.tsx', '**/**/*.ts'], // Target TypeScript files
        languageOptions: {
            ecmaVersion: 2021, // Latest ECMAScript features
            sourceType: 'module', // Use ES Modules syntax
            globals: {
                browser: true,
                commonjs: true, // Support CommonJS globals like `require`
                node: true, // Node.js globals like `process`
                console: 'readonly',
                process: 'readonly',
            },
            parser: tsParser, // Use TypeScript parser
            parserOptions: {
                project: './tsconfig.json', // Required for type-checking rules
                tsconfigRootDir: __dirname,
            },
        },
        plugins: {
            '@typescript-eslint': tsPlugin,
            import: require('eslint-plugin-import'), // Add import plugin
        },
        rules: {
            // General rules
            indent: ['error', 4],
            'max-len': ['error', { code: 320 }],
            'no-console': ['error', { allow: ['warn', 'error'] }],
            semi: ['error', 'never'],

            // TypeScript-specific rules
            '@typescript-eslint/no-unused-vars': ['error'],
            '@typescript-eslint/no-explicit-any': 'warn',
            '@typescript-eslint/explicit-function-return-type': 'off',

            // Import rules
            'import/no-unresolved': 'warn', // Enable no-unresolved rule
        },
    },
    {
        // Apply to JavaScript files
        files: ['**/*.js'], // Target JavaScript files
        languageOptions: {
            ecmaVersion: 2021,
            sourceType: 'commonjs', // Use CommonJS syntax
            globals: {
                browser: true,
                commonjs: true,
                node: true, // Node.js globals
                console: 'readonly',
                process: 'readonly',
            },
        },
        rules: {
            // General rules
            indent: ['error', 4],
            'max-len': ['error', { code: 320 }],
            'no-console': ['error', { allow: ['warn', 'error'] }],
            semi: ['error', 'never'],
        },
    },
]

:scroll: Additional Scripts

add-service.sh

The add-service.sh script automates the creation and setup of new services in a Serverless Framework-based project, ensuring consistent configurations.

Features:

  1. Directory Structure:
  • Creates folders for handlers, utilities, and documentation.
  • Adds a .env file for environment variables.
  1. Configuration Files:
  • serverless.yml: Sets up the service and its functions.
  • tsconfig.json: Configures TypeScript settings.
  • swagger.json & dredd.yml: Prepares API documentation and testing setup.
  • hooks.py: Placeholder for Dredd hooks.
  1. Source Files:
  • Includes a sample handler and utility function.
  1. serverless-compose.yml Integration:
  • Updates or creates serverless-compose.yml to register the new service.

Usage:

Run the script with the desired service name as an argument:

./add-service.sh <service-name>

For example:

./add-service.sh my-service

This creates a service named my-service with all necessary files and updates the serverless-compose.yml file.

5 Likes

@deepak-cardoza Great job! Can you also explore serverless-plugin-typescript

Sure @ranjith-n-7edge

1 Like

Adding some context from our experience — when we tried using the Serverless TypeScript plugin earlier, the build output bundled most helpers into single handlers, which made the compiled code hard to read and debug. We also noticed increased handler sizes and limited flexibility compared to our manual TSC-based build process.

That said, tooling evolves over time. If anyone has tried newer versions or found ways to optimize readability, bundle size, or debugging with the Serverless TypeScript plugin, please share. Let’s learn together :slightly_smiling_face:

1 Like