Advanced Serverless Techniques I: Do Not Repeat Yourself
We’re diving into innovative strategies to refine scalability, enhance reusability, and perfect our project’s structure with our latest series, “Mastering Serverless with Advanced Techniques.” After laying a solid foundation in serverless architecture, it’s time to take things up a notch. Our journey begins with a fresh approach to structuring Lambdas. As we wrap up our foundational serverless series, we’ve equipped you with a well-architected Lambda setup, complete with dependency injection for DynamoDB interactions, sophisticated logging tools, and middleware for thorough validation. Yet, the duplication across different Lambda functions poses a challenge to the DRY principle — Don’t Repeat Yourself. Let’s explore a new strategy to address this.
Pre-Requisite
You may need to read some of the articles of the “Mastering Serverless” series:
- Mastering Serverless (Part I): Enhancing DynamoDB Interactions with Document Client
- Mastering Serverless (Part II): Mastering AWS DynamoDB Batch Write Failures for a Smoother Experience.
- Mastering Serverless (Part III): Enhancing AWS Lambda and DynamoDB Interactions with Dependency Injection
- Mastering Serverless IV: Unit Testing DynamoDB Dependency Injection With Jest
- Mastering Serverless (Part V): Advanced Logging Techniques for AWS Lambda
Current Approach
Before we dive deeper, make sure you’re caught up with our previous articles on mastering serverless. Understanding the interaction with AWS services through dependency injection, the role of middleware like middy, and the utilities AWS provides is crucial. Typically, a Lambda function for one of our APIs might look something like this:
async function lambdaHandler(event) {
const TableName = process.env.bacaSupportersTableName;
const {username} = event.pathParameters;
try {
// Interaction with DynamoDB
const {Items: supporters=[]} = await dynamodbQueryHelper(
TableName,
"#hashKey = :hashKey",
{"#hashKey": "createdBy"},
{":hashKey": username}
);
logger.info(`Supporters obtained: ${supporters?.length}`);
// Sending the Results
return _200(supporters.sort((a, b) => a?.name?.localeCompare(b?.name)));
} catch (e) {
// Managing errors
logger.error(e.message);
return _500({status: false, message: e.message});
}
}
export const handler = middy(lambdaHandler)
.use(injectLambdaContext(logger))
// Code to do validation
.use(inputValidator({
validateEnv: ['bacaSupportersTableName'],
// validatePathParameters: ['username'],
}));
This snippet illustrates how we interact with DynamoDB, manage logging, and handle errors — a pattern repeated across many of our Lambdas, leading us to rethink our approach.
Creating a Base Handler
To streamline our Lambda functions, I’m introducing a base handler that encapsulates common functionalities such as logging, input validation, and error handling. This allows each Lambda to inherit from this base, focusing on the unique logic needed for its specific task.
Explaining the Base Handler Code
'use strict';
import middy from '@middy/core';
import { MsaLogger as logger } from "../../libs/logger.js";
import { _200, _500 } from "../../libs/response-helper.js";
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import { inputValidator } from "../../libs/middleware/input-validator.js";
export class BaseHandler {
constructor() {
this.handler = middy(this.handle.bind(this))
.use(injectLambdaContext(logger))
.use(inputValidator(this.getValidatorConfig()));
}
async handle(event) {
// Common pre-processing
try {
const result = await this.main(event);
return _200(result);
} catch (e) {
logger.error(e.message);
return _500({ status: false, message: e.message });
}
// Common post-processing
}
async main(event) {
// To be overridden by subclasses
}
getValidatorConfig() {
// Common validator config or to be overridden by subclasses
}
}
This code snippet showcases our BaseHandler class. It’s designed to handle common pre-processing, such as logging and input validation, and post-processing tasks. The main
method is where the unique logic of each Lambda will be defined, making this a powerful tool for reducing redundancy and focusing on what matters most in each function.
Extending the Base Handler & Injecting DynamoDB Service
To further enhance our serverless architecture, creating a custom middleware for DynamoDB operations allows us to encapsulate database logic, making it reusable and easy to maintain across different Lambdas. This middleware injects an instance of the DynamoService into the Lambda context, simplifying database interactions within your handlers.
Middleware and Lambda Handler Implementation
// dynamoServiceMiddleware.js
import { DynamoService } from 'path-to-dynamo-service'; // Adjust the path accordingly
export const dynamoServiceMiddleware = () => {
let dynamoService;
return {
before: async (handler) => {
if (!dynamoService) {
dynamoService = new DynamoService();
}
handler.context.dynamoService = dynamoService;
}
};
};
import middy from '@middy/core';
import { BaseHandler } from 'path-to-your-base-handler'; // Adjust the path accordingly
import { dynamoServiceMiddleware } from 'path-to-dynamo-service-middleware'; // Adjust the path
class MyLambdaHandler extends BaseHandler {
async main(event) {
const dynamoService = this.context.dynamoService;
const TableName = process.env.bacaSupportersTableName;
const { username } = event.pathParameters;
// Use the dynamoService for database operations
const response = await dynamoService.dynamodbGetHelper({ TableName, Key: { username } });
// Process the response and return
// ...
}
// ...
}
export const handler = new MyLambdaHandler().handler.use(dynamoServiceMiddleware());
This implementation demonstrates how the dynamoServiceMiddleware seamlessly integrates DynamoDB operations into our Lambda functions. By injecting the DynamoService instance before the handler executes, we maintain clean, maintainable code that’s easy to reuse across projects.
Conclusion
In this series, we’ve embarked on a journey to refine our serverless architecture with advanced techniques. By rethinking our code structure, we’ve unlocked valuable lessons that not only enhance our current projects but also provide insights applicable across a wide range of development scenarios. Emphasizing progressive improvement, we continue to evolve our codebase, seeking the most effective strategies to achieve efficiency, maintainability, and scalability. Join us as we push the boundaries of serverless architecture, ensuring our skills and projects remain at the cutting edge.
Empower Your Tech Journey:
Explore a wealth of knowledge designed to elevate your tech projects and understanding. From safeguarding your applications to mastering serverless architecture, discover articles that resonate with your ambition.
New Projects or Consultancy
For new project collaborations or bespoke consultancy services, reach out directly and let’s transform your ideas into reality. Ready to take your project to the next level?
- Email me at one@upskyrocket.com
- Visit My Partner In Tech for custom solutions
Protecting Routes
- How to Create Protected Routes Using React, Next.js, and AWS Amplify
- How to Protect Routes for Admins in React Next.js Using HOC
- Secure Your Next.js App: Advanced User Management with AWS Cognito Groups
Advanced Serverless Techniques
- Advanced Serverless Techinques I: Do Not Repeat Yourself
- Advanced Serverless Techniques II: Streamlining Data Access with DAL
- Advanced Serverless Techniques III: Simplifying Lambda Functions with Custom DynamoDB Middleware
- Advanced Serverless Techniques IV: AWS Athena for Serverless Data Analysis
- Advanced Serverless Techniques V: DynamoDB Streams vs. SQS/SNS to Lambda
- Advanced Serverless Techniques VI: Building Resilient and Efficient Cloud Architectures With AWS SNS, Lambda, and DynamoDB Streams
Mastering Serverless Series
- Mastering Serverless (Part I): Enhancing DynamoDB Interactions with Document Client
- Mastering Serverless (Part II): Mastering AWS DynamoDB Batch Write Failures for a Smoother Experience.
- Mastering Serverless (Part III): Enhancing AWS Lambda and DynamoDB Interactions with Dependency Injection
- Mastering Serverless IV: Unit Testing DynamoDB Dependency Injection With Jest
- Mastering Serverless (Part V): Advanced Logging Techniques for AWS Lambda