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.
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.
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.
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:
- Before packaging for deployment (
sls deploy). - 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.
Build Script: ts-build.sh
The build script performs the following actions:
- Copies shared resources (
lib/,entities/) into each service. - Runs the TypeScript compiler (
tsc) for the specific service. - 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."
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:
- Separate Rules for TypeScript and JavaScript:
- TypeScript files (
.ts,.tsx) get special handling with the@typescript-eslintplugin and parser. - JavaScript files (
.js) follow best practices with standard ESLint rules.
- CommonJS and ES Modules Support:
- The setup works seamlessly with both
require-based andimport/export-based modules.
- Customizable Rules:
- For example, no
console.logis allowed, butconsole.warnandconsole.errorare 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'],
},
},
]
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:
- Directory Structure:
- Creates folders for handlers, utilities, and documentation.
- Adds a
.envfile for environment variables.
- 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.
- Source Files:
- Includes a sample handler and utility function.
serverless-compose.ymlIntegration:
- Updates or creates
serverless-compose.ymlto 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.