AWS: SAM Introduction
Serverless is one of the most exciting ways to build modern cloud applications β and AWS SAM makes it even easier.
What is AWS SAM?
AWS Serverless Application Model (SAM) is an open-source framework that helps you build and deploy serverless applications on AWS.
Itβs designed to simplify your infrastructure-as-code, especially when working with:
- AWS Lambda
- API Gateway
- DynamoDB
- EventBridge, SQS, Step Functions, and more
At its core, SAM is just a shorthand syntax for AWS CloudFormation β making your templates cleaner, easier to write, and faster to iterate.
Why Use AWS SAM?
You should consider SAM when:
- Youβre building serverless applications using AWS services like Lambda and API Gateway.
- You want to define infrastructure as code but find raw CloudFormation too verbose.
- You want to test Lambda functions locally using Docker.
- You prefer guided deployment over manually zipping and uploading code.
SAM is ideal for:
- Quick prototyping of serverless apps
- Developer teams who want simplicity without giving up AWS-native IaC
- Learning how serverless works with real AWS infrastructure
SAM vs. CloudFormation: what’s the difference?
SAM is built on top of CloudFormation, so it inherits all the benefits of CloudFormation while providing a simpler syntax for serverless applications. Here are some key differences:
Feature | AWS CloudFormation | AWS SAM |
---|---|---|
Purpose | Define any AWS infrastructure | Focused on serverless apps |
Syntax | YAML/JSON (verbose) | Simplified YAML with shorthand |
Testing | β No built-in local testing | β Local testing with Docker |
Deployment CLI | aws cloudformation deploy | sam deploy –guided |
Abstraction Layer | Base layer | Built on top of CloudFormation |
In short: SAM is CloudFormation β just way easier for serverless.
You still get all the benefits of CloudFormation (rollback, drift detection, etc.), but with less effort and boilerplate.
SAM Main Components
- template.yaml
Your SAM template is the blueprint of your application β it declares all the AWS resources your app needs.
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.12
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
- samconfig.toml: Configuration for Reusability & Environments
When you run sam deploy --guided
, SAM generates a samconfig.toml
file. This file stores deployment settings like your S3 bucket, stack name, region, and parameter overrides β so you donβt need to type them every time.
But beyond that, you can define multiple environments using named configurations. Example:
version = 0.1
[staging.deploy.parameters]
stack_name = "my-sam-app-staging"
region = "ap-southeast-1"
s3_bucket = "my-sam-artifacts-staging"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=staging"
[prod.deploy.parameters]
stack_name = "my-sam-app-prod"
region = "ap-southeast-1"
s3_bucket = "my-sam-artifacts-prod"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=prod"
Now you can deploy using:
sam deploy --config-env staging
sam deploy --config-env prod
This allows:
- Cleaner separation between dev/staging/prod
- Safer deployment practices
- Per-env overrides for Lambda environment variables, tags, etc.
- SAM CLI
A command-line tool that simplifies development and deployment:
sam init
β scaffold a new projectsam build
β package your codesam deploy
β push it to AWSsam local invoke
β test individual functions locallysam local start-api
β emulate full API locally
If youβre starting your journey into serverless with AWS, SAM is one of the best tools to learn and use. It removes the friction of writing raw CloudFormation, supports local development, and lets you ship your ideas quickly.
Itβs not just beginner-friendly β itβs also powerful enough to be used in production systems, especially when paired with other AWS services like DynamoDB, Step Functions, and EventBridge.
AWS: Lambda Basics
AWS Lambda is one of the most exciting services in the serverless world. It lets you write code that automatically responds to events β without needing to worry about provisioning servers or managing infrastructure.
In this post, I will cover the basics:
- What is Lambda?
- What are the core components?
- Why use it?
- A real use case: processing SQS messages in TypeScript
- Common limitations
What is AWS Lambda?
AWS Lambda is a serverless compute service that lets you run code in response to events without provisioning or managing servers. You can use Lambda to run code for virtually any type of application or backend service with zero administration. Just upload your code and Lambda takes care of everything required to run and scale your code with high availability.
You donβt manage servers. You just focus on the code, and Lambda takes care of:
- Running it
- Scaling it automatically
- Charging you only when it runs
Key Components of AWS Lambda
- Handlers: The entry point for your Lambda function. It’s the method that AWS Lambda calls to start execution.
- Events: Lambda functions are triggered by events, which can come from various AWS services like S3, DynamoDB, API Gateway, or even custom events.
- Context: Provides runtime information to your Lambda function, such as the function name, version, and remaining execution time.
- IAM Roles: AWS Identity and Access Management (IAM) roles define the permissions for your Lambda function, allowing it to access other AWS services securely.
- Environment Variables: Key-value pairs that you can use to pass configuration settings to your Lambda function at runtime.
- Timeouts and Memory: You can configure the maximum execution time and memory allocated to your Lambda function, which affects performance and cost.
- CloudWatch Logs: Automatically logs the output of your Lambda function, which you can use for debugging and monitoring.
Why Use AWS Lambda?
- Cost-Effective: You only pay for the compute time you consume. There are no charges when your code is not running.
- Scalability: Automatically scales your application by running code in response to each event, so you donβt have to worry about scaling your infrastructure.
- Flexibility: Supports multiple programming languages (Node.js, Python, Java, Go, C#, Ruby, and custom runtimes) and can be used for a wide range of applications, from simple scripts to complex microservices.
- Event-Driven: Easily integrates with other AWS services, allowing you to build event-driven architectures that respond to changes in your data or system state.
- Zero Administration: No need to manage servers or runtime environments. AWS handles all the infrastructure management tasks, including patching, scaling, and availability.
Real Use Case: TypeScript Lambda to Process SQS β DynamoDB
In my current role, we use AWS Lambda to process messages from SQS queues. Hereβs a simple example of how you can set up a Lambda function in TypeScript to process messages from an SQS queue and store them in DynamoDB.
Lets say we receive messages in SQS that contain user data, and we want to store this data in DynamoDB.
import { SQSHandler, SQSEvent, Context } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
const dynamoDb = new DynamoDB.DocumentClient();
export const handler: SQSHandler = async (
event: SQSEvent,
context: Context
) => {
for (const record of event.Records) {
const userData = JSON.parse(record.body);
const params = {
TableName: "Users",
Item: userData,
};
await dynamoDb.put(params).promise();
}
};
In this example:
- We import necessary types from
aws-lambda
and theDynamoDB
client fromaws-sdk
. - The
handler
function processes each message in the SQS event. - We parse the message body and store it in a DynamoDB table named
Users
.
This function will be uploaded to AWS Lambda, and you can configure it to trigger whenever new messages arrive in the SQS queue.
Common Limitations of AWS Lambda
- Execution Time: Lambda functions have a maximum execution time of 15 minutes. If your task takes longer, you may need to break it into smaller functions or use other services.
- Cold Starts: When a Lambda function is invoked after being idle, it may take longer to start due to the initialization time (cold start). This can affect performance, especially for latency-sensitive applications.
- Limited Resources: Each Lambda function has a maximum memory limit (up to 1024 MB) and a maximum package size (50 MB for direct upload, 250 MB when using layers). This can be a constraint for resource-intensive applications.
- Limited Runtime Environment: While Lambda supports multiple programming languages, you may encounter limitations with certain libraries or dependencies that require a specific runtime environment.
- State Management: Lambda functions are stateless, meaning they do not retain any state between invocations. If you need to maintain state, you will have to use external storage solutions like DynamoDB or S3.
- Concurrency Limits: There are limits on the number of concurrent executions for Lambda functions. If your application experiences a sudden spike in traffic, you may hit these limits, leading to throttling of requests.
- Vendor Lock-In: Using AWS Lambda ties you to the AWS ecosystem, which can make it challenging to migrate to other cloud providers or on-premises in the future.
Wrap-Up
AWS Lambda is a powerful tool for building serverless applications that can scale automatically and respond to events without the need for server management. By understanding its core components and limitations, you can effectively leverage Lambda to build efficient, cost-effective applications that meet your business needs.
Whether youβre processing SQS messages, building APIs with API Gateway, or integrating with other AWS services, Lambda provides a flexible and scalable solution that can adapt to your applicationβs requirements.
AWS: DynamoDB Basics
Datastore is always a crucial part of any application, and choosing the right database can significantly impact your application’s performance, scalability, and maintainability. In this post, we’ll explore AWS DynamoDB.
Database Types
There are two main types of databases:
- Relational Databases (RDBMS): These databases use structured query language (SQL) and are designed to handle structured data with predefined schemas. Examples include MySQL, PostgreSQL, and Oracle.
- NoSQL Databases: These databases are designed to handle unstructured or semi-structured data. They provide flexibility in data modeling and can scale horizontally. Examples include MongoDB, Cassandra, and DynamoDB.
If you’re coming from MySQL or PostgreSQL, imagine removing JOINs and replacing rows with JSON-like documents stored under a single key.
Key Features of DynamoDB
- Fully Managed: DynamoDB is a fully managed service, meaning AWS handles the operational aspects such as hardware provisioning, setup, configuration, and scaling.
- Performance at Scale: It automatically scales up and down to adjust for capacity and maintain performance.
- Flexible Data Model: Supports key-value and document data structures, allowing for a variety of use cases.
- Built-in Security: Offers encryption at rest and in transit, along with fine-grained access control.
So, what is DynamoDB? DynamoDB is a fully managed NoSQL database service provided by AWS that offers fast and predictable performance with seamless scalability. It is designed to handle large amounts of data and high request rates, making it ideal for applications that require low-latency data access.
Key Concepts
- Tables: The primary structure in DynamoDB, similar to tables in relational databases. Each table has a primary key that uniquely identifies each item.
- Items: Individual records in a table, similar to rows in a relational database.
- Attributes: The data fields in an item, similar to columns in a relational database.
- Primary Key: Uniquely identifies each item in a table. It can be a simple primary key (partition key) or a composite primary key (partition key and sort key).
- Indexes: Allow for efficient querying of data. DynamoDB supports both global secondary indexes (GSI) and local secondary indexes (LSI).
A simple item in a DynamoDB table might look like this:
{
"UserId": "12345", # Unique identifier for the user. Primary key.
"Name": "Hazriq",
"Email": "hazriq@example.com"
}
Benefits of Using DynamoDB
- Scalability: Automatically scales to handle large amounts of data and high request rates without manual intervention.
- Performance: Provides low-latency data access, making it suitable for real-time applications.
- Flexibility: Supports various data models, allowing developers to choose the best fit for their application.
- Cost-Effective: Pay-as-you-go pricing model, where you only pay for the resources you use, making it cost-effective for applications with variable workloads.
- Integration with AWS Services: Seamlessly integrates with other AWS services like Lambda, API Gateway, and CloudWatch for monitoring and logging.
- TTL (Time to Live): Automatically deletes expired items, helping manage storage costs and data lifecycle.
Performance Considerations
- Provisioned Throughput: You can specify the read and write capacity units for your table, which determines how many reads and writes per second your table can handle.
- On-Demand Capacity: Automatically scales to accommodate workload changes, making it suitable for unpredictable workloads.
- Caching: Use DynamoDB Accelerator (DAX) for in-memory caching to improve read performance for read-heavy workloads.
- Batch Operations: Use batch operations for efficient processing of multiple items in a single request, reducing the number of round trips to the database.
Without DAX
- Reads and writes are directly from the DynamoDB table.
- Each read or write operation incurs a latency based on the network and DynamoDB’s processing time.
With DAX
- DAX acts as an in-memory cache, reducing the latency for read operations.
- DAX handles cache misses by fetching data from DynamoDB and storing it in memory for subsequent requests.
- This significantly speeds up read operations, especially for frequently accessed data.
When not to Use DynamoDB
While DynamoDB is a powerful tool, it may not be the best fit for every use case. Here are some scenarios where you might consider alternatives:
- Complex Queries: If your application requires complex queries with multiple joins or aggregations, a relational database might be more suitable.
- Transactional Support: If your application requires complex transactions involving multiple items or tables, consider using a relational database or a database that supports multi-item transactions.
- Large Binary Objects: If your application needs to store large binary objects (BLOBs), such as images or videos, consider using Amazon S3 for storage and DynamoDB for metadata.
- High Write Throughput: If your application requires extremely high write throughput, consider using Amazon S3 or a distributed database like Apache Cassandra.
DynamoDB shines when you need a fast, scalable, and fully managed database that just works β whether you’re powering a real-time leaderboard, handling millions of API requests, or storing user sessions with minimal latency. By understanding its core concepts and performance features like DAX, you can unlock a powerful tool that fits right into modern, serverless-first architectures.
Of course, like any tool, itβs not a one-size-fits-all solution. Knowing when and how to use DynamoDB effectively is key β and that journey starts with grasping its strengths.
AWS: SQS vs SNS
At my new company, we rely heavily on AWS SQS (Simple Queue Service) and SNS (Simple Notification Service) to handle the large volume of records we need to ingest daily.
Think of a situation where the system experiences a temporary bottleneck or needs to go offline for maintenance or upgrades. In such cases, SQS acts as a bufferβsafely storing incoming records in a queue so theyβre not lost. Even during traffic spikes, the messages are queued and processed at our own pace. For example, our AWS Lambda function polls the queue and retrieves messages when it’s ready, allowing the system to remain responsive and scalable even under pressure.
Core Components of SQS and SNS
To better understand how SQS and SNS work, it helps to break down their main components.
π¨ SQS Main Components:
- Queue: The main container where messages are stored temporarily until processed.
- Producer: The system or service that sends messages to the queue.
- Consumer: The service (e.g. Lambda, EC2) that polls the queue and processes the message.
- Visibility Timeout: A short period where the message becomes invisible to other consumers once picked upβhelps avoid duplicate processing.
- DLQ (Dead-Letter Queue): A separate queue that stores messages that couldn’t be successfully processed after several retry attempts. Useful for debugging failed messages.
π£ SNS Main Components:
- Topic: The central component that receives messages from publishers.
- Publisher: The producer that sends a message to a topic.
- Subscriber: Services or endpoints (like Lambda, SQS queue, HTTPS endpoint, email) that receive messages pushed from the topic.
- Subscription Filter Policy: You can apply rules to decide which subscribers should receive which messages (useful for message routing).
SQS vs SNS β What’s the Difference?
While both SQS and SNS are messaging services provided by AWS, they serve very different purposes:
Feature | SQS (Simple Queue Service) | SNS (Simple Notification Service) |
---|---|---|
Message Pattern | Point-to-point (Queue-based) | Publish/Subscribe (Fan-out) |
Delivery Target | Message goes to one consumer | Message goes to multiple subscribers |
Storage | Messages are stored temporarily in a queue | Messages are pushed immediately, not stored by default |
Use Case | Decouple producer and consumer; reliable message handling | Broadcast messages to multiple endpoints/services |
Consumer Behaviour | Consumers poll the queue | Subscribers receive push notifications |
- Use SQS when you want to decouple your producer and consumer, especially if the consumer might be temporarily unavailable.
- Use SNS when you want to broadcast messages to multiple services (e.g., notify a Lambda, an email service, and an HTTP endpoint at the same time).
JS: Express.js
Express.js
is a minimalist and flexible Node.js
web application framework that provides a robust set of features for building web and mobile applications and APIs. If you’re looking to build fast, scalable, and efficient server-side applications with JavaScript, Express.js is your go-to choice. Its unopinionated nature gives you immense freedom,
Setting Up
Getting started with Express.js is straightforward. If you already have a Node.js project, or if you’re starting a new one, follow these simple steps:
- Initializing a Node.js project (
npm init
). - Installing Express.js (
npm install express
).
Now, you’re ready to create your first Express server!
Core Concepts
Understanding these fundamental concepts is key to mastering Express.js:
Routing
Routing is the process of determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, PUT, DELETE, etc.).
In Express.js, you define routes using methods corresponding to HTTP verbs on the app object (an instance of Express).
HTTP Methods (Verbs): These map directly to common operations:
GET
: Retrieve data (e.g., fetching a list of users).POST
: Submit data to be processed (e.g., creating a new user).PUT
: Update existing data (e.g., modifying a user’s details).DELETE
: Remove data (e.g., deleting a user).
const express = require("express");
const app = express();
const port = 3000;
// GET request to the root URL
app.get("/", (req, res) => {
res.send("Hello from Express!");
});
// GET request to /users
app.get("/users", (req, res) => {
res.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
});
// POST request to /users
app.post("/users", (req, res) => {
// Logic to create a new user
res.status(201).send("User created successfully!");
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
// Route Parameters:
// Example URL: /users/123
app.get("/users/:id", (req, res) => {
const { id } = req.params;
res.send(`Fetching user with ID: ${id}`);
});
// Query Parameters:
// Example URL: /search?q=nodejs&category=backend
app.get("/search", (req, res) => {
const { q: searchTerm, category } = req.query;
res.send(`Searching for "${searchTerm}" in "${category}" category.`);
});
Middleware
Middleware functions are core to Express.js. They are functions that have access to the request
object (req
), the response object (res
), and the next()
function in the application’s request-response cycle. The next()
function is crucial as it passes control to the next middleware function.
Think of middleware as a series of steps a request goes through before reaching its final route handler.
-
How Middleware Works: Requests flow sequentially through middleware functions. Each function can perform operations, modify
req
orres
, end the cycle by sending a response, or pass control to the next middleware vianext()
. -
Common Use Cases:
- Logging: Recording details about incoming requests.
- Authentication/Authorization: Verifying user credentials and permissions.
- Body Parsing: Express itself doesn’t parse request bodies by default. Middleware like
express.json()
andexpress.urlencoded()
are essential for handling JSON and URL-encoded form data. - Serving Static Files: Handled by express.static().
- Error Handling: Special middleware for catching and processing errors.
Example of Custom Middleware:
// A simple logger middleware
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Pass control to the next handler
};
app.use(logger); // Use the middleware for all incoming requests
app.get("/", (req, res) => {
res.send("Home Page with Logger!");
});
Handling Request (req) and Response (res)
req
(Request Object):- Accessing request headers (
req.header
). - Accessing route and query parameters (
req.params
,req.query
). - Accessing the request body (
req.body
).
- Accessing request headers (
res
(Response Object):- Sending various types of responses:
res.send()
,res.json()
,res.sendFile()
. - Setting HTTP status codes:
res.status()
. - Redirecting requests:
res.redirect()
. - Chaining methods (e.g.,
res.status(200).json(...)
).
- Sending various types of responses:
Typical Project Structure
While Express is unopinionated, a well-organized project structure is crucial for maintainability and scalability, especially as your application grows. Here’s a common pattern:
my-express-app/
βββ node_modules/
βββ server.js # Entry point: Starts the server, sets up environment
βββ app.js # Express app configuration: Defines middleware, connects routes
βββ routes/ # Defines API endpoints and links to controllers
β βββ auth.routes.js
β βββ user.routes.js
βββ controllers/ # Contains request handlers: Logic for specific routes, interacts with services
β βββ auth.controller.js
β βββ user.controller.js
βββ services/ # Business logic: Handles complex operations, orchestrates data flow
β βββ auth.service.js
β βββ user.service.js
βββ models/ # Data schema definitions (e.g., Mongoose schemas, Sequelize models)
β βββ User.js
β βββ Product.js
βββ middlewares/ # Custom middleware functions
β βββ auth.middleware.js
β βββ error.middleware.js
βββ config/ # Configuration files (database settings, environment variables)
β βββ db.config.js
βββ package.json
βββ package-lock.json
βββ .env # Environment variables
Sample Project
( coming soon )
PY: defaultdict()
A defaultdict
is a subclass of dict that calls a factory function to supply missing values for any requested key.
… In today’s call, I was looking at a simple python solution that will try to count the number of fruits based on types.
Lets take this example. I want to know how many numbers apple in the list.
The typical way:
fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counts = {}
for fruit in fruit_list:
if fruit in fruit_counts:
fruit_counts[fruit] += 1
else:
fruit_counts[fruit] = 1
print(fruit_counts)
# Output: {'apple': 3, 'banana': 2, 'orange': 1}
We will check the fruit_counts
dict. If it’s there, add more. If not, set it it to 1.
Looks simple, but apparently… There’s a way to do this in more pythonic way..
from collections import defaultdict
fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counts = defaultdict(int) # defaultdict with a default factory of int (which returns 0)
for fruit in fruit_list:
fruit_counts[fruit] += 1 # If 'fruit' is not in fruit_counts, it defaults to 0, then 1 is added.
print(fruit_counts)
# Output: defaultdict(<class 'int'>, {'apple': 3, 'banana': 2, 'orange': 1})
If fruit is already a key in fruit_counts, its current value is incremented by 1. If fruit is not yet a key in fruit_counts, defaultdict(int) automatically creates fruit_counts[fruit] and initializes its value to 0. Then, 1 is added to it, making its value 1.
From GeeksforGeeks:
- Using
int
: If you use int as the factory function, the default value will be 0 (since int() returns 0). - Using
list
: If you use list as the factory function, the default value will be an empty list ([]). - Using
str
: If you use str, the default value will be an empty string (’’).
Now… What if I want to start from 100? Apparently… we can! via lambda…
from collections import defaultdict
fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counts = defaultdict(lambda: 50)
for fruit in fruit_list:
Β Β # If 'fruit' is not in fruit_counts, it defaults to 50, then 1 is added.
Β Β fruit_counts[fruit] += 1
print(fruit_counts)
# defaultdict(<function <lambda> at 0x7f73165e7d30>, {'apple': 53, 'banana': 52, 'orange': 51})
My First Post
Hellow World!