<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ serverless - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ serverless - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 25 May 2026 05:05:47 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/serverless/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Serverless Spam Classifier Using Scikit-Learn, AWS Lambda, & API Gateway ]]>
                </title>
                <description>
                    <![CDATA[ In today's digital world, spam is no longer just an annoyance - it's a growing security threat. To combat this, developers often turn to machine learning to build intelligent filters that can distingu ]]>
                </description>
                <link>https://www.freecodecamp.org/news/deploying-serverless-spam-classifier/</link>
                <guid isPermaLink="false">69f2e347b18c978233780179</guid>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MathJax ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Data Architecture ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Rakshath Naik ]]>
                </dc:creator>
                <pubDate>Thu, 30 Apr 2026 05:06:15 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/08672d22-a4df-4b99-8ef7-fffd18f5dc07.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In today's digital world, spam is no longer just an annoyance - it's a growing security threat. To combat this, developers often turn to machine learning to build intelligent filters that can distinguish legitimate emails from malicious ones.</p>
<p>While building a machine learning model in a notebook is relatively straightforward, the real challenge lies in the last mile: deploying that model into a scalable, production-ready system that users can actually interact with.</p>
<p>In this project, I built an end-to-end serverless spam classifier, combining Scikit-learn for model development with AWS Lambda, Amazon S3, and Amazon API Gateway for deployment. The result is a lightweight, scalable API that can classify messages in real time.</p>
<p>The system is designed to be modular and cost-efficient, allowing the model to be retrained and updated independently without affecting the live API. From detecting "free iPhone" scams to identifying phishing attempts, this project demonstrates how to bridge the gap between machine learning experimentation and real-world deployment.</p>
<h3 id="heading-table-of-contents">Table of&nbsp;Contents</h3>
<ul>
<li><p><a href="#heading-1-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-2-building-the-brain-the-model">Building the Brain: The Model</a></p>
</li>
<li><p><a href="#heading-3-deploying-the-model-to-aws">Deploying the Model to AWS</a></p>
</li>
<li><p><a href="#heading-4-how-to-run-the-project-locally">How to Run The Project Locally</a></p>
</li>
<li><p><a href="#heading-5-our-project-architecture">Our Project Architecture</a></p>
</li>
<li><p><a href="#heading-6-conclusion-the-power-of-serverless-ai">Conclusion: The Power of Serverless AI</a></p>
</li>
<li><p><a href="#heading-7-acknowledgment-references">Acknowledgment / References</a></p>
</li>
</ul>
<h2 id="heading-1-prerequisites">1. Prerequisites</h2>
<ol>
<li><p><strong>Fundamental skills:</strong> Basic proficiency in Python and understanding of Machine Learning concepts like classification.</p>
</li>
<li><p><strong>AWS account:</strong> Access to an AWS account with permissions for Lambda, S3, and API Gateway.</p>
</li>
<li><p><strong>Environment:</strong> Python 3.11 installed, along with libraries like scikit-learn, pandas, and joblib.</p>
</li>
<li><p><strong>AWS CLI:</strong> Configured on your local machine for file uploads.</p>
</li>
<li><p><strong>HuggingFace account:</strong> You can directly download the model from my account.</p>
</li>
</ol>
<h2 id="heading-2-building-the-brain-the-model">2. Building the Brain: The&nbsp;Model</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6942c2903c5d674e359eaf1e/b43af198-1472-4914-9469-6cd5ca5384e2.png" alt="Demonstrational image to show the brain of AI." style="display:block;margin:0 auto" width="1000" height="563" loading="lazy">

<p><em>Photo by</em> <a href="https://unsplash.com/@steve_j?utm_source=medium&amp;utm_medium=referral"><em>Steve A Johnson</em></a> <em>on</em> <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral"><em>Unsplash</em></a></p>
<p>At the heart of this project lies a supervised learning approach. Instead of simply specifying which words are considered spam, we'll provide the computer with a dataset and an algorithm, enabling it to learn and identify spam patterns on its own.</p>
<h3 id="heading-1-vectorization-turning-text-into-math">1. Vectorization: Turning Text into&nbsp;Math</h3>
<p>Machine Learning models can't <strong>read</strong> text. They require numerical input. To solve this, we used the <a href="https://www.freecodecamp.org/news/how-to-extract-keywords-from-text-with-tf-idf-and-pythons-scikit-learn-b2a0f3d7e667/">TF-IDF</a> (Term Frequency-Inverse Document Frequency) Vectorizer.</p>
<pre><code class="language-python">feature_extraction = TfidfVectorizer(min_df=1, stop_words='english', lowercase=True)
X_train_features = feature_extraction.fit_transform(X_train
</code></pre>
<p>Here's the mathematical formula:</p>
<p>$$w_{i,j} = tf_{i,j} \times \log \left( \frac{N}{df_i} \right)$$</p>
<p>TF-IDF term definitions:</p>
<ul>
<li><p><strong>wᵢ,ⱼ (Weight):</strong> The final importance score of a specific word in a document.</p>
</li>
<li><p><strong>tfᵢ,ⱼ (Term Frequency):</strong> How often a word appears in a single email.</p>
</li>
<li><p><strong>N (Total Documents):</strong> The total count of all emails in your dataset.</p>
</li>
<li><p><strong>dfᵢ (Document Frequency):</strong> The number of different emails that contain this specific word.</p>
</li>
<li><p><strong>log(N/dfᵢ) (IDF):</strong> A penalty that lowers the score of common words like <strong>the</strong> or <strong>is</strong> that appear everywhere.</p>
</li>
</ul>
<p>It cleans the data by removing common words, converts all text to lowercase for consistency, and assigns more importance to rare and meaningful words while giving less importance to frequently used words.</p>
<h3 id="heading-2-training-the-logistic-regression-engine">2. Training: The Logistic Regression Engine</h3>
<p>We'll use <strong>Logistic Regression</strong> here, a classification algorithm that predicts the probability of an outcome.</p>
<p>In this stage, we feed our vectorized training data into the Logistic Regression algorithm. The goal is to establish a mathematical relationship between specific word weights and the <strong>Spam</strong> or <strong>Ham</strong> label.</p>
<p>During training, the model iteratively adjusts its internal parameters to minimize error, eventually learning that words like winner or free correlate highly with spam, while conversational language correlates with legitimate messages.</p>
<pre><code class="language-python">model = LogisticRegression()
model.fit(X_train_features, Y_train)
</code></pre>
<p>In our case, it calculates the probability that an email belongs to spam or HAM.</p>
<p>The algorithm uses the Sigmoid function to map any real-valued number into a value between 0 and 1.</p>
<p>$$P(y=1|x) = \frac{1}{1 + e^{-(z)}}$$</p>
<p>where z = β₀ + β₁x₁ +&nbsp;… + βₙxₙ.</p>
<h3 id="heading-3-evaluation-testing-the-intelligence">3. Evaluation: Testing the Intelligence</h3>
<p>After training, we need to verify if the brain actually works on data it hasn't seen before.</p>
<pre><code class="language-python">prediction_on_test_data = model.predict(X_test_features)
accuracy_on_test_data = accuracy_score(Y_test, prediction_on_test_data)
</code></pre>
<p>By comparing the model’s predictions against the actual labels in our test set, we calculate an Accuracy Score. This gives us the confidence that the model is ready for the real world (achieving ~94% accuracy in our tests).</p>
<h3 id="heading-4-exporting-the-logic-serialization">4. Exporting the Logic (Serialization)</h3>
<p>To move this brain from our local Python environment to the AWS Cloud, we'll use Joblib to save our work into binary files (.pkl).</p>
<pre><code class="language-python">joblib.dump(model, 'spam_model.pkl')
joblib.dump(feature_extraction, 'vectorizer.pkl')
</code></pre>
<p>We use the Pickle format because it allows us to freeze complex Python objects (mathematical weights and word mappings) into a portable binary format that can be instantly re-animated in the cloud.</p>
<p>We need the Vectorizer to translate new user text into the exact numerical coordinates the Model was trained to understand. Using one without the other is like having a key but no lock.</p>
<p>The trained Logistic Regression model and TF-IDF vectorizer are openly available for the community on Hugging Face here: <a href="https://huggingface.co/rakshath1/mail-spam-detector">Get the model on HuggingFace</a>.</p>
<h2 id="heading-3-deploying-the-model-to-aws">3. Deploying the Model to&nbsp;AWS</h2>
<p>Training a model is science, while deploying it is engineering. To make this classifier accessible to the world, we'll use a serverless stack that scales automatically and incurs nearly no maintenance costs.</p>
<h3 id="heading-1-model-storage-amazon-s3">1. Model Storage: Amazon&nbsp;S3</h3>
<p>First, we'll uploade our&nbsp;.pkl files to an S3 bucket. By decoupling the model from the code, we can update the AI's intelligence (simply by overwriting the file in S3) without redeploying the backend code. It makes the system highly maintainable.</p>
<h3 id="heading-2-the-production-backend-aws-lambda">2. The Production Backend: AWS&nbsp;Lambda</h3>
<p>To make the AI accessible, we'll move from a local script to a Serverless Cloud Architecture. This ensures the model is always available without the cost of a 24/7 server.</p>
<p>The deployment environment is AWS Lambda (Python 3.11). Since Lambda is a lightweight environment, it doesn't include Scikit-Learn or Joblib. To provide these, we'll download and store them in our S3 bucket and import them through the layers.</p>
<p><strong>Commands in AWS CLI:</strong></p>
<pre><code class="language-python">
# 1. Create a workspace
mkdir ml_layer &amp;&amp; cd ml_layer

# 2. Install scikit-learn and its dependencies into a folder
pip install \
    --platform manylinux2014_x86_64 \
    --target=python/lib/python3.11/site-packages \
    --implementation cp \
    --python-version 3.11 \
    --only-binary=:all: \
    scikit-learn joblib

# 3. Zip the folder
zip -r sklearn_lib.zip python

# 4. Upload to S3 (Using AWS CLI)
aws s3 cp sklearn_lib.zip s3://YOUR-BUCKET-NAME/
</code></pre>
<p>We store the Scikit-Learn library as a ZIP in S3 to bypass the AWS Lambda deployment package size limit. This allows the function to dynamically load heavy dependencies only when needed without bloating the core code.</p>
<p><strong>The Lambda Function:</strong></p>
<pre><code class="language-python">
import json
import boto3
import os
import sys
from io import BytesIO

# Ensures the custom Lambda layer(containing sklearn/joblib)
sys.path.append('/opt/python')

try:
    import joblib
except ImportError:
    # Fallback for specific Scikit-Learn distributions
    from sklearn.utils import _joblib as joblib

# Initialize S3 client
s3 = boto3.client('s3')

# Use placeholders for the article so readers can insert their own values
BUCKET_NAME = 'YOUR_S3_BUCKET_NAME' 
MODEL_KEY = 'spam_model.pkl'
VECTORIZER_KEY = 'vectorizer.pkl'

# Global variables for 'Warm Start' caching (improves performance by keeping model in RAM)
model = None
vectorizer = None

def load_model():
    """Downloads model files from S3 only if they aren't already in RAM"""
    global model, vectorizer
    if model is None or vectorizer is None:
        try:
            # 1. Load the Logistic Regression Model from S3
            m_obj = s3.get_object(Bucket=BUCKET_NAME, Key=MODEL_KEY)
            model = joblib.load(BytesIO(m_obj['Body'].read()))
            
            # 2. Load the TF-IDF Vectorizer directly from S3
            v_obj = s3.get_object(Bucket=BUCKET_NAME, Key=VECTORIZER_KEY)
            vectorizer = joblib.load(BytesIO(v_obj['Body'].read()))
        except Exception as e:
            raise Exception(f"Failed to load .pkl files from S3: {str(e)}")

def lambda_handler(event, context):
    try:
        # Ensure model and vectorizer are ready before processing
        load_model()
        
        # Handles both direct Lambda tests and API Gateway POST requests
        body = event.get('body', event)
        if isinstance(body, str):
            body = json.loads(body)
            
        text = body.get('text', '')
            
        if not text:
            return {
                'statusCode': 400,
                'body': json.dumps({'error': 'No text provided.'})
              }

        # 1. Transform input text to numeric features using the trained Vectorizer
        data_vec = vectorizer.transform([text])
        
        # 2. Predict using the Logistic Regression Model 
        prediction = int(model.predict(data_vec)[0])
        
      # 3. Map numeric result to human-readable label
        result_label = "HAM" if prediction == 1 else "SPAM"
        
        # RESPONSE WITH CORS
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*' # needed for cross-domain web integration
            },
            'body': json.dumps({
                'status': 'success',
                'classification': result_label,
                'input_text': text
            })
        }
        
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error_message': f"Inference Error: {str(e)}"})
        }
</code></pre>
<p>Key features of the Lambda function:</p>
<ol>
<li><p><strong>Warm start caching:</strong> By defining the model and vectorizer variables outside the lambda_handler, we store them in the container's memory. This significantly reduces cold start latency for subsequent requests.</p>
</li>
<li><p><strong>Dynamic dependency loading:</strong> The <strong>sys.path.append('/opt/python')</strong> line allows us to import heavy libraries from S3/Layers without exceeding the upload limit.</p>
</li>
<li><p><strong>Bimodal input handling:</strong> The function is designed to handle both direct JSON testing from the AWS console and stringified payloads sent via API Gateway.</p>
</li>
</ol>
<h3 id="heading-3-the-api-gateway-the-bridge-to-the-web">3. The API Gateway - The Bridge to the&nbsp;Web</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6942c2903c5d674e359eaf1e/8aa3e8d7-569a-4dd5-a6ac-184922474952.png" alt="Demonstrational image to show the API Gateway." style="display:block;margin:0 auto" width="1000" height="563" loading="lazy">

<p>Photo by <a href="https://unsplash.com/@growtika?utm_source=medium&amp;utm_medium=referral">Growtika</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></p>
<h4 id="heading-creating-the-rest-api">Creating the REST API</h4>
<p>Next we'll create a REST API with a single POST method. Why POST, you might be wondering? Well, we need to securely send a JSON payload containing the user’s text message to our model.</p>
<ol>
<li><p>First navigate to the Amazon API Gateway console and select Create API -&gt; REST API.</p>
</li>
<li><p>Give your API a name, such as EmailSpamPredictor-API, and set the Endpoint Type to Regional.</p>
</li>
<li><p>Then in the left sidebar, click Resources and enter a resource name (e.g: <strong>/ predict</strong> as entered by me)</p>
</li>
<li><p>Next click the create method and select POST and then select Lambda Function for integration type</p>
</li>
<li><p>Ensure Lambda Proxy integration is enabled (this allows the full request to pass through to your code).</p>
</li>
</ol>
<p><strong>The CORS Configuration (The Troubleshooting Hub)</strong><br>This is where many developers encounter the dreaded <strong>Connection Error</strong>. Since our API is hosted on AWS, and if your front-end is on a separate website, the browser’s Same-Origin Policy will block the request by default.</p>
<p>To fix this, we'll enable <strong>CORS:</strong></p>
<ol>
<li><p><strong>Access-Control-Allow-Origin:</strong> Set to * (or specifically to your domain) to tell the browser that the API is allowed to talk to your front-end.</p>
</li>
<li><p><strong>The OPTIONS method:</strong> API Gateway creates an OPTIONS method automatically. This handles the Preflight request where the browser asks, “Are you allowed to receive data from me?” before sending the actual text.</p>
</li>
<li><p><strong>Access-Control-Allow-Headers:</strong> In the screenshot, you'll notice headers like Content-Type and Authorization are allowed. This ensures that when our JavaScript fetch() call sets the content type to application/json, the API Gateway doesn't reject it.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6942c2903c5d674e359eaf1e/cf5c87c6-f374-4dda-8001-77a0aab52672.png" alt="Image illustrates the CORS configuration for our project. " style="display:block;margin:0 auto" width="1487" height="617" loading="lazy">

<p>Image illustrates the CORS configuration for our project. (Image by author)</p>
<h4 id="heading-deployment-stages">Deployment Stages</h4>
<p>Once the API is deployed to a production stage, AWS generates a permanent Invoke URL. This acts as the public gateway to our model and typically follows this structure: <a href="https://%5Bapi-id%5D.execute-api.%5Bregion%5D.amazonaws.com/prod/classify">https://[api-id].execute-api.[region].amazonaws.com/prod/classify</a>.</p>
<h4 id="heading-connecting-the-frontend-the-javascript-layer">Connecting the Frontend (The JavaScript Layer)</h4>
<p>With the API live, we can now write a simple JavaScript function to talk to our model. This script runs whenever a user clicks the <strong>Analyze</strong> button on your site.</p>
<pre><code class="language-python">
async function checkSpam() {
    const message = document.getElementById("userInput").value;
    const apiUrl = "YOUR_API_GATEWAY_INVOKE_URL";

    try {
        const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({ "text": message })
        });

        const data = await response.json();
        
        // Display result on the webpage
        const resultElement = document.getElementById("result");
        resultElement.innerText = `Prediction: ${data.classification}`;
        resultElement.style.color = data.classification === "SPAM" ? "red" : "green";

    } catch (error) {
        console.error("Error:", error);
        alert("Could not connect to the Spam Detector API.");
    }
}
</code></pre>
<h2 id="heading-4-how-to-run-the-project-locally">4. How to Run The Project&nbsp;Locally</h2>
<p>You can store the front-end as an HTML file. Once it's ready, you shouldn’t just double-click the&nbsp;.html file. Opening it as a <strong>file</strong> in your browser can cause security restrictions. Instead, you should host it using a simple local server.</p>
<p><strong>Step 1:</strong> Open the terminal or Command Prompt.</p>
<p><strong>Step 2:</strong> Navigate to your project folder</p>
<pre><code class="language-shell">cd [PATH_TO_YOUR_FOLDER]
</code></pre>
<p><strong>Step 3:</strong> Start a local Python web server.</p>
<pre><code class="language-shell">python -m http.server 8000
</code></pre>
<p><strong>Step 4:</strong> Access the application.</p>
<p>Open your browser and navigate to:<br><a href="http://localhost:8000/your-file-name.html">http://localhost:8000/your-file-name.html</a></p>
<p><strong>Watch the Demo:</strong></p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/q2X_azntmzY" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-5-our-project-architecture">5. Our Project Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6942c2903c5d674e359eaf1e/c17673d4-5dd0-43dc-8e8d-3015bcd31864.png" alt="Image showing the Architecture Diagram of our Project." style="display:block;margin:0 auto" width="1000" height="563" loading="lazy">

<p>The image illustrates the architecture of our project (Building a Serverless Spam Classifier). It shows the process that takes place from the client input to the final model output. (Image by Author)</p>
<ol>
<li><p><strong>Client Front-End Interaction:</strong> The process starts on the far left. A user interacts with the web interface (for example, a website or a desktop app). They input text like <strong>WIN free iPhone now</strong> and trigger a request.</p>
</li>
<li><p><strong>The Entry Point: API Gateway:</strong> The request hits the Amazon API Gateway, which acts as the <strong>security guard</strong> and translator.&nbsp;<br><strong>(a)</strong> CORS OPTIONS handles the pre-flight handshake to ensure the browser has permission to talk to the AWS cloud.&nbsp;<br><strong>(b)</strong> Classification Request (POST) routes the actual message data to your backend logic.</p>
</li>
<li><p><strong>The Engine: AWS Lambda (Python 3.11):</strong>&nbsp;The central “<strong>lightbulb</strong>” represents your Lambda function. This is where the code you wrote lives. It doesn’t run 24/7 – it only wakes up when a request arrives.</p>
</li>
<li><p><strong>Storage &amp; Retrieval: S3 Bucket:</strong> Since Lambda is lightweight, it doesn’t store your heavy Machine Learning files internally.<br><strong>Dependency and Model Download:</strong> The function reaches out to the S3 Bucket to pull in the sklearn_<a href="http://lib.zip">lib.zip</a> (the engine) and the&nbsp;.pkl files (the intelligence).&nbsp;<br><strong>Required Dependency and Model:</strong> These assets are loaded into the Lambda’s temporary memory to prepare for the prediction.</p>
</li>
<li><p><strong>The Inference Pipeline:</strong>&nbsp;Inside the Lambda, a three-step mathematical cycle occurs:<br><strong>(a) Text Vectorizer:</strong> Translates the words into numbers.<br><strong>(b) Logistic Regression:</strong> Calculates the probability of spam based on those numbers.<br><strong>(c) Label:</strong> Assigns a final result (Spam or Ham).</p>
</li>
<li><p><strong>The Result Delivery:</strong> The result is sent back through the API Gateway, including the necessary CORS Headers to ensure the browser accepts it. The front-end then updates to show the “<strong>Result: SPAM</strong>” with a visual indicator.</p>
</li>
</ol>
<h2 id="heading-6-conclusion-the-power-of-serverless-ai">6. Conclusion: The Power of Serverless AI</h2>
<p>By merging the mathematical simplicity of Logistic Regression with the industrial strength of AWS Serverless Architecture, we have transformed a static Python script into a globally accessible, scalable API.</p>
<p>This project demonstrates that you don’t need a massive budget or a 24/7 dedicated server to deploy high-quality Machine Learning.</p>
<p>Using the S3-to-Lambda workaround allowed us to bypass common storage hurdles, ensuring that our Brain (the model) and its Muscle (Scikit-Learn) could function seamlessly within the cloud’s ephemeral environment. It bridges the gap between experimentation and real-world applications, making AI systems practical, efficient, and accessible.</p>
<h2 id="heading-7-acknowledgment-references">7. Acknowledgment / References</h2>
<ul>
<li><p>Pre-trained spam classification model: View on Hugging Face (<a href="https://huggingface.co/rakshath1/mail-spam-detector"><strong>rakshath1/mail-spam-detector · Hugging Face</strong></a><strong>)</strong></p>
</li>
<li><p>Scikit-learn <a href="https://scikit-learn.org/stable/api/index.html?utm_source=chatgpt.com">Documentation</a></p>
</li>
<li><p>AWS Lambda <a href="https://docs.aws.amazon.com/lambda/latest/api/welcome.html?utm_source=chatgpt.com">Documentation</a></p>
</li>
<li><p>Amazon S3 <a href="https://aws.amazon.com/documentation-overview/s3/">Documentation</a></p>
</li>
<li><p>Amazon API Gateway <a href="https://docs.aws.amazon.com/apigateway/">Documentation</a></p>
</li>
</ul>
<h3 id="heading-connect-with-me">Connect With Me</h3>
<ul>
<li><p><a href="https://medium.com/@rakshathnaik62">Medium</a></p>
</li>
<li><p><a href="https://www.linkedin.com/in/rakshath-/">LinkedIN</a></p>
</li>
</ul>
<p><strong>You may also like</strong></p>
<ol>
<li><p><a href="https://qubrica.com/python-polars-v-s-pandas-libraries-comparison/">How Polars overtook Pandas</a></p>
</li>
<li><p><a href="https://qubrica.com/devops-is-dead-platform-engineering-2026/"><strong>DevOps is Dead. Long Live Platform Engineering</strong></a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Full-Stack CRUD App with React, AWS Lambda, DynamoDB, and Cognito Auth ]]>
                </title>
                <description>
                    <![CDATA[ Building a web application that works only on your local machine is one thing. Building one that is secure, connected to a real database, and accessible to anyone on the internet is another challenge  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/full-stack-aws-react-lambda-dynamodb-tutorial/</link>
                <guid isPermaLink="false">69b96f7ec22d3eeb8ac3bf81</guid>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ full stack ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Benedicta Onyebuchi ]]>
                </dc:creator>
                <pubDate>Tue, 17 Mar 2026 15:13:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1a996eff-72f5-4f4d-b8da-cf4d646c3224.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Building a web application that works only on your local machine is one thing. Building one that is secure, connected to a real database, and accessible to anyone on the internet is another challenge entirely. And it requires a different set of tools.</p>
<p>Most production web applications share a common set of needs: they store and retrieve data, they expose that data through an API, they require users to authenticate before accessing sensitive operations, and they need to be deployed somewhere reliable and fast.</p>
<p>Meeting all of those needs used to require managing servers, configuring databases, handling authentication infrastructure, and provisioning hosting environments – often as separate, manual processes.</p>
<p>AWS changes that model significantly. With the combination of services you'll use in this tutorial (Lambda, DynamoDB, API Gateway, Cognito, and CloudFront), you can build and deploy a fully functional, secured, globally distributed application without managing a single server.</p>
<p>Each service handles one specific responsibility:</p>
<ul>
<li><p>DynamoDB stores your data</p>
</li>
<li><p>Lambda runs your business logic on demand</p>
</li>
<li><p>API Gateway exposes your functions as a REST API</p>
</li>
<li><p>Cognito manages user authentication</p>
</li>
<li><p>CloudFront delivers your frontend worldwide over HTTPS.</p>
</li>
</ul>
<p>The AWS CDK (Cloud Development Kit) ties all of this together by letting you define every one of those services as TypeScript code. Instead of clicking through the AWS Console to configure each resource manually, you describe your entire infrastructure in a single file and deploy it with one command.</p>
<p>By the end of this tutorial, you will have a fully deployed vendor management dashboard. Users can sign up, log in, and then create, read, and delete vendors, with all data securely stored in AWS DynamoDB and all routes protected by Amazon Cognito authentication.</p>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<p>In this handbook, you'll build a two-panel web app where authenticated users can:</p>
<ul>
<li><p>Add a new vendor (name, category, contact email)</p>
</li>
<li><p>View all saved vendors in real time</p>
</li>
<li><p>Delete a vendor from the list</p>
</li>
<li><p>Sign in and sign out securely</p>
</li>
</ul>
<p>The frontend is built with Next.js. The backend runs entirely on AWS: DynamoDB stores the data, Lambda functions handle the logic, API Gateway exposes a REST API, Cognito manages authentication, and CloudFront serves the app globally over HTTPS.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-who-this-is-for">Who This Is For</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a href="#heading-part-1-set-up-your-aws-account-and-tools">Part 1: Set Up Your AWS Account and Tools</a></p>
</li>
<li><p><a href="#heading-part-2-set-up-the-project-structure">Part 2: Set Up the Project Structure</a></p>
</li>
<li><p><a href="#heading-part-3-define-the-database-dynamodb">Part 3: Define the Database (DynamoDB)</a></p>
</li>
<li><p><a href="#heading-part-4-write-the-lambda-functions">Part 4: Write the Lambda Functions</a></p>
</li>
<li><p><a href="#heading-part-5-build-the-api-with-api-gateway">Part 5: Build the API with API Gateway</a></p>
</li>
<li><p><a href="#heading-part-6-deploy-the-backend-to-aws">Part 6: Deploy the Backend to AWS</a></p>
</li>
<li><p><a href="#heading-part-7-build-the-react-frontend">Part 7: Build the React Frontend</a></p>
</li>
<li><p><a href="#heading-part-8-add-authentication-with-amazon-cognito">Part 8: Add Authentication with Amazon Cognito</a></p>
</li>
<li><p><a href="#heading-part-9-deploy-the-frontend-with-s3-and-cloudfront">Part 9: Deploy the Frontend with S3 and CloudFront</a></p>
</li>
<li><p><a href="#heading-what-you-built">What You Built</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-who-this-is-for">Who This Is For</h2>
<p>This tutorial is for developers who know basic JavaScript and React but have never used AWS. You don't need any prior backend, cloud, or DevOps experience. I'll explain every AWS concept before we use it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, make sure you have the following installed and available:</p>
<ul>
<li><p><strong>Node.js 18 or higher</strong>: <a href="https://nodejs.org">Download here</a></p>
</li>
<li><p><strong>npm</strong>: Included with Node.js</p>
</li>
<li><p><strong>A code editor</strong>: I recommend VS Code</p>
</li>
<li><p><strong>A terminal</strong>: Any terminal on macOS, Linux, or Windows (WSL recommended on Windows)</p>
</li>
<li><p><strong>An AWS account</strong>: You will create one in Part 1. A credit card is required, but the Free Tier covers everything in this tutorial.</p>
</li>
<li><p><strong>Basic familiarity with React and TypeScript</strong>: You should understand components, <code>useState</code>, and <code>useEffect</code>.</p>
</li>
</ul>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Before writing any code, here's a plain-English description of how the pieces fit together.</p>
<p>When a user clicks "Add Vendor" in the React app:</p>
<ol>
<li><p>The frontend reads the user's JWT auth token from the browser session</p>
</li>
<li><p>It sends a <code>POST</code> request to API Gateway, including the token in the request header</p>
</li>
<li><p>API Gateway checks the token against Cognito. If the token is invalid or missing, it rejects the request with a 401 error immediately</p>
</li>
<li><p>If the token is valid, API Gateway passes the request to the createVendor Lambda function</p>
</li>
<li><p>The Lambda function writes the new vendor to DynamoDB</p>
</li>
<li><p>DynamoDB confirms the write, and the Lambda returns a success response</p>
</li>
<li><p>The frontend re-fetches the vendor list and updates the UI</p>
</li>
</ol>
<p>The same flow applies to reading and deleting vendors, with different Lambda functions and HTTP methods.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/70486bdc-f272-45db-be30-f10752916546.png" alt="Architecture diagram of the Vendors Tracker Application" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><strong>How the app is deployed:</strong> Your React app is exported as a static site, uploaded to an S3 bucket, and served globally through CloudFront. Your backend infrastructure (Lambda functions, API Gateway, DynamoDB, Cognito) is defined in TypeScript using AWS CDK and deployed with a single command.</p>
<h2 id="heading-part-1-set-up-your-aws-account-and-tools">Part 1: Set Up Your AWS Account and Tools</h2>
<p>Before writing any application code, you need three things in place: an AWS account, the right tools on your machine, and credentials that let those tools communicate with AWS on your behalf.</p>
<h3 id="heading-11-create-your-aws-account">1.1 Create Your AWS Account</h3>
<p>If you don't have an AWS account:</p>
<ol>
<li><p>Go to <a href="https://aws.amazon.com">https://aws.amazon.com</a></p>
</li>
<li><p>Click <strong>Create an AWS Account</strong></p>
</li>
<li><p>Follow the sign-up prompts and add a payment method</p>
</li>
<li><p>Once registered, log in to the AWS Management Console</p>
</li>
</ol>
<p>AWS has a Free Tier that covers all the services used in this tutorial. You won't be charged for normal use while following along.</p>
<h3 id="heading-12-install-the-aws-cli-and-cdk">1.2 Install the AWS CLI and CDK</h3>
<p>The <strong>AWS CLI</strong> is a command-line tool that lets you interact with AWS from your terminal: checking resources, configuring credentials, and more.</p>
<p>The <strong>AWS CDK (Cloud Development Kit)</strong> is the tool you will use to define your entire backend (database, Lambda functions, API) using TypeScript code. Instead of clicking through the AWS Console to create each resource, you describe what you want in a TypeScript file and CDK builds it for you.</p>
<p>Install both:</p>
<pre><code class="language-shell"># Install AWS CLI (macOS)
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

# For Linux, see: https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html
# For Windows, see: https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-windows.html

# Install AWS CDK globally
npm install -g aws-cdk
</code></pre>
<p>Verify both are installed:</p>
<pre><code class="language-shell">aws --version
cdk --version
</code></pre>
<p>Both commands should print a version number. If they do, you are ready to move on.</p>
<h3 id="heading-13-configure-your-aws-credentials-iam">1.3 Configure Your AWS Credentials (IAM)</h3>
<p>This step is critical. Your terminal needs a set of credentials – like a username and password – to act on your behalf inside AWS.</p>
<p>Think of your root account (the one you signed up with) as the master key to your entire AWS account. You should never use it for day-to-day development. Instead, you will create a separate IAM user with its own set of keys. If those keys are ever exposed, you can delete them without compromising your root account.</p>
<h4 id="heading-phase-1-create-an-iam-user">Phase 1: Create an IAM User</h4>
<ol>
<li><p>Log in to the AWS Console and search for IAM in the top search bar</p>
</li>
<li><p>In the left sidebar, click Users, then click Create user</p>
</li>
<li><p>Name the user <code>cdk-dev</code>. Leave "Provide user access to the AWS Management Console" unchecked – you only need terminal access, not console access</p>
</li>
<li><p>On the permissions screen, choose Attach policies directly</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/d4699108-c1aa-4dd3-957c-b84292c719a2.png" alt="IAM Console showing the “Attach policies directly” screen with AdministratorAccess checked" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<ol>
<li>Search for <code>AdministratorAccess</code> and check the box next to it</li>
</ol>
<p>Note on permissions: In a production job you would use a more restricted policy. For this tutorial, Administrator access is needed because CDK creates many different types of AWS resources.</p>
<p>6. Click through to the end and click Create user</p>
<h4 id="heading-phase-2-generate-access-keys">Phase 2: Generate Access Keys</h4>
<ol>
<li><p>Click on your newly created <code>cdk-dev</code> user from the Users list</p>
</li>
<li><p>Go to the Security credentials tab</p>
</li>
<li><p>Scroll down to Access keys and click Create access key</p>
</li>
<li><p>Select Command Line Interface (CLI), check the acknowledgment box, and click Next</p>
</li>
<li><p>Click Create access key</p>
</li>
</ol>
<p><strong>Important</strong>: Copy both the Access Key ID and the Secret Access Key right now. You will never be able to see the Secret Access Key again after closing this screen. Save both values in a password manager or secure note.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/d85bb4eb-0ecf-4d92-be92-d75af5a534c6.png" alt="IAM Console showing the Create access key screen with the Access Key ID and Secret Access Key" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h4 id="heading-phase-3-connect-your-terminal-to-aws">Phase 3: Connect Your Terminal to AWS</h4>
<p>Run the following command in your terminal:</p>
<pre><code class="language-shell">aws configure
</code></pre>
<p>You will be prompted for four values:</p>
<pre><code class="language-shell">AWS Access Key ID:     [paste your Access Key ID]
AWS Secret Access Key: [paste your Secret Access Key]
Default region name:   us-east-1
Default output format: json
</code></pre>
<p>Use <code>us-east-1</code> as your region for this tutorial. After this step, every CDK and AWS CLI command you run will use these credentials automatically.</p>
<h2 id="heading-part-2-set-up-the-project-structure">Part 2: Set Up the Project Structure</h2>
<p>You will use a <strong>monorepo</strong> layout – one top-level folder with two sub-projects inside: <code>frontend</code> for your React app and <code>backend</code> for your AWS infrastructure code. They are deployed independently but live side by side.</p>
<h3 id="heading-21-create-the-workspace">2.1 Create the Workspace</h3>
<pre><code class="language-shell">mkdir vendor-tracker &amp;&amp; cd vendor-tracker
mkdir backend frontend
</code></pre>
<h3 id="heading-22-initialize-the-frontend-nextjs">2.2 Initialize the Frontend (Next.js)</h3>
<p>Navigate into the <code>frontend</code> folder and run:</p>
<pre><code class="language-shell">cd frontend
npx create-next-app@latest .
</code></pre>
<p>When prompted, choose the following options:</p>
<ul>
<li><p><strong>TypeScript</strong> --&gt; Yes</p>
</li>
<li><p><strong>ESLint</strong> --&gt; Yes</p>
</li>
<li><p><strong>Tailwind CSS</strong> --&gt; Yes</p>
</li>
<li><p><strong>src/ directory</strong> --&gt;No</p>
</li>
<li><p><strong>App Router</strong> --&gt; Yes</p>
</li>
<li><p><strong>Import alias</strong> --&gt; No</p>
</li>
</ul>
<h3 id="heading-23-initialize-the-backend-cdk">2.3 Initialize the Backend (CDK)</h3>
<p>Navigate into the <code>backend</code> folder and run:</p>
<pre><code class="language-shell">cd ../backend
cdk init app --language typescript
</code></pre>
<p>This generates a boilerplate CDK project. The most important file it creates is <code>backend/lib/backend-stack.ts</code>. This is where you will define all of your AWS infrastructure as TypeScript code.</p>
<p>Also install <code>esbuild</code>, which CDK uses to bundle your Lambda functions:</p>
<pre><code class="language-shell">npm install --save-dev esbuild
</code></pre>
<h3 id="heading-24-understanding-cdk-before-you-write-any-code">2.4 Understanding CDK Before You Write Any Code</h3>
<p>CDK is likely different from most tools you have used. Here is how it works:</p>
<p>Normally, you would create AWS resources by clicking through the AWS Console: create a table here, configure a Lambda function there. CDK lets you do all of that using TypeScript code instead.</p>
<p>When you run <code>cdk deploy</code>, CDK reads your TypeScript file, converts it into an AWS CloudFormation template (an internal AWS format for describing infrastructure), and submits it to AWS. AWS then creates all the resources you described.</p>
<p>A few terms you will see throughout this tutorial:</p>
<ul>
<li><p><strong>Stack</strong>: The collection of all AWS resources you define together. Your <code>BackendStack</code> class is your stack.</p>
</li>
<li><p><strong>Construct</strong>: Each individual AWS resource you create inside a stack (a table, a Lambda function, an API) is called a construct.</p>
</li>
<li><p><strong>Deploy</strong>: Running <code>cdk deploy</code> sends your TypeScript definition to AWS and creates or updates the real resources.</p>
</li>
</ul>
<p>The main file you'll work in is <code>backend/lib/backend-stack.ts</code>. Think of it as the blueprint for your entire backend.</p>
<p>Your final project structure will look like this:</p>
<pre><code class="language-plaintext">vendor-tracker/
├── backend/
│   ├── lambda/
│   │   ├── createVendor.ts
│   │   ├── getVendors.ts
│   │   └── deleteVendor.ts
│   ├── lib/
│   │   └── backend-stack.ts
│   └── package.json
└── frontend/
    ├── app/
    │   ├── layout.tsx
    │   ├── page.tsx
    │   └── providers.tsx
    ├── lib/
    │   └── api.ts
    ├── types/
    │   └── vendor.ts
    └── .env.local
</code></pre>
<h2 id="heading-part-3-define-the-database-dynamodb">Part 3: Define the Database (DynamoDB)</h2>
<p>DynamoDB is AWS's NoSQL database. Think of it as a fast, scalable key-value store in the cloud. Every item in a DynamoDB table must have a unique ID called the <strong>partition key</strong>. For your vendor table, that key will be <code>vendorId</code>.</p>
<p>Open <code>backend/lib/backend-stack.ts</code>. Replace the entire file contents with the following:</p>
<pre><code class="language-typescript">import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. DynamoDB Table
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: {
        name: 'vendorId',
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // For development only
    });
  }
}
</code></pre>
<p><strong>What each line does:</strong></p>
<ul>
<li><p><code>partitionKey</code> tells DynamoDB that <code>vendorId</code> is the unique identifier for every record. No two vendors can share the same <code>vendorId</code>.</p>
</li>
<li><p><code>PAY_PER_REQUEST</code> means you only pay when data is actually read or written. There is no charge when the table is idle, which makes it cost-effective for learning.</p>
</li>
<li><p><code>RemovalPolicy.DESTROY</code> means the table will be deleted when you run <code>cdk destroy</code>. For production apps you would not use this.</p>
</li>
</ul>
<h2 id="heading-part-4-write-the-lambda-functions">Part 4: Write the Lambda Functions</h2>
<p>A Lambda function is your server, but unlike a traditional server, it only runs when it's called. AWS spins it up on demand, runs your code, and shuts it down. You're only charged for the time your code is actually running.</p>
<p>You'll write three Lambda functions:</p>
<ul>
<li><p><code>createVendor.ts</code>: Adds a new vendor to DynamoDB</p>
</li>
<li><p><code>getVendors.ts</code>: Returns all vendors from DynamoDB</p>
</li>
<li><p><code>deleteVendor.ts</code>: Removes a vendor from DynamoDB by ID</p>
</li>
</ul>
<p>Create a new folder inside <code>backend</code>:</p>
<pre><code class="language-shell">mkdir backend/lambda
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/6330a84b-77c3-4001-9783-5fedc89ae1c0.png" alt="6330a84b-77c3-4001-9783-5fedc89ae1c0" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-a-note-on-the-aws-sdk">A Note on the AWS SDK</h3>
<p>All three Lambda functions use <strong>AWS SDK v3</strong> (<code>@aws-sdk/client-dynamodb</code> and <code>@aws-sdk/lib-dynamodb</code>). This is the current standard. An older version of the SDK (<code>aws-sdk</code>) exists but is deprecated and not bundled in the Node.js 18 Lambda runtime, which is what you'll use. Stick to v3 throughout.</p>
<h3 id="heading-41-create-vendor-lambda">4.1 Create Vendor Lambda</h3>
<p>Create <code>backend/lambda/createVendor.ts</code>:</p>
<pre><code class="language-typescript">import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async (event: any) =&gt; {
  try {
    const body = JSON.parse(event.body);

    const item = {
      vendorId: randomUUID(), // Generates a collision-safe unique ID
      name: body.name,
      category: body.category,
      contactEmail: body.contactEmail,
      createdAt: new Date().toISOString(),
    };

    await docClient.send(
      new PutCommand({
        TableName: process.env.TABLE_NAME!,
        Item: item,
      })
    );

    return {
      statusCode: 201,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
      },
      body: JSON.stringify({ message: "Vendor created", vendorId: item.vendorId }),
    };
  } catch (error) {
    console.error("Error creating vendor:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "Failed to create vendor" }),
    };
  }
};
</code></pre>
<p><strong>What each part does:</strong></p>
<ul>
<li><p><code>randomUUID()</code> generates a universally unique ID using Node's built-in <code>crypto</code> module. No extra package is needed. This is more reliable than <code>Date.now()</code>, which can produce duplicate IDs if two requests arrive within the same millisecond.</p>
</li>
<li><p><code>process.env.TABLE_NAME</code> reads the DynamoDB table name from an environment variable. You'll set this value in the CDK stack. This avoids hardcoding the table name inside your Lambda code.</p>
</li>
<li><p>The <code>headers</code> block is required for CORS (Cross-Origin Resource Sharing). Without <code>Access-Control-Allow-Origin</code>, your browser will block responses from a different domain than your frontend. Without <code>Access-Control-Allow-Headers</code>, the <code>Authorization</code> header you add later for Cognito will be rejected during the browser's preflight check.</p>
</li>
</ul>
<h3 id="heading-42-get-vendors-lambda">4.2 Get Vendors Lambda</h3>
<p>Create <code>backend/lambda/getVendors.ts</code>:</p>
<pre><code class="language-typescript">import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async () =&gt; {
  try {
    const response = await docClient.send(
      new ScanCommand({
        TableName: process.env.TABLE_NAME!,
      })
    );

    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(response.Items ?? []),
    };
  } catch (error) {
    console.error("Error fetching vendors:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "Failed to fetch vendors" }),
    };
  }
};
</code></pre>
<p><strong>What each part does:</strong></p>
<ul>
<li><p><code>ScanCommand</code> reads every item in the table and returns them as an array. For a learning project this is fine. In a production app with millions of rows, you would use a more targeted <code>QueryCommand</code> to avoid reading the entire table on every request.</p>
</li>
<li><p><code>response.Items ?? []</code> returns an empty array if the table is empty, preventing the frontend from crashing when there are no vendors yet.</p>
</li>
</ul>
<h3 id="heading-43-delete-vendor-lambda">4.3 Delete Vendor Lambda</h3>
<p>Create <code>backend/lambda/deleteVendor.ts</code>:</p>
<pre><code class="language-typescript">import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async (event: any) =&gt; {
  try {
    const body = JSON.parse(event.body);
    const { vendorId } = body;

    if (!vendorId) {
      return {
        statusCode: 400,
        headers: { "Access-Control-Allow-Origin": "*" },
        body: JSON.stringify({ error: "vendorId is required" }),
      };
    }

    await docClient.send(
      new DeleteCommand({
        TableName: process.env.TABLE_NAME!,
        Key: { vendorId },
      })
    );

    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
      },
      body: JSON.stringify({ message: "Vendor deleted" }),
    };
  } catch (error) {
    console.error("Error deleting vendor:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "Failed to delete vendor" }),
    };
  }
};
</code></pre>
<p><strong>What each part does:</strong></p>
<ul>
<li><p><code>DeleteCommand</code> removes the item whose <code>vendorId</code> matches the key you provide. DynamoDB doesn't return an error if the item doesn't exist. It simply does nothing.</p>
</li>
<li><p>The <code>400</code> guard at the top returns a clear error if the caller forgets to send a <code>vendorId</code>, rather than letting DynamoDB throw a confusing internal error.</p>
</li>
</ul>
<h2 id="heading-part-5-build-the-api-with-api-gateway">Part 5: Build the API with API Gateway</h2>
<p>API Gateway is what gives your Lambda functions a public URL. Without it, there's no way for your browser to trigger a Lambda function. Think of it as the front door of your backend: it receives HTTP requests, checks whether the caller is authorized, routes the request to the correct Lambda, and returns the Lambda's response to the caller.</p>
<p>Now you'll wire everything together in <code>backend/lib/backend-stack.ts</code>.</p>
<h3 id="heading-51-add-lambda-functions-and-api-gateway-to-the-stack">5.1 Add Lambda Functions and API Gateway to the Stack</h3>
<p>Replace the entire contents of <code>backend/lib/backend-stack.ts</code> with this complete, assembled file:</p>
<pre><code class="language-typescript">import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. DynamoDB Table 
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: {
        name: 'vendorId',
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // 2. Lambda Functions
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // 3. Permissions (Least Privilege)
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // 4. API Gateway
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda));
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda));
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda));

    // 5. Outputs
    new cdk.CfnOutput(this, 'ApiEndpoint', {
      value: api.url,
    });
  }
}
</code></pre>
<p><strong>What each section does:</strong></p>
<p><code>NodejsFunction</code> is a special CDK construct that automatically bundles your Lambda code and all its dependencies into a single file using <code>esbuild</code> before uploading it to AWS. This is why you installed <code>esbuild</code> in Part 2.</p>
<p>Always use <code>NodejsFunction</code> instead of the basic <code>lambda.Function</code> construct. The basic version requires you to manually manage bundling, which causes "Module not found" errors at runtime.</p>
<p><strong>Permissions (Least Privilege):</strong> In AWS, no resource can communicate with any other resource by default. A Lambda function has no access to DynamoDB, S3, or anything else unless you explicitly grant it.</p>
<p>This is called the <strong>Least Privilege</strong> principle: each piece of your system gets exactly the permissions it needs, and nothing more. <code>grantWriteData</code> lets a Lambda write and delete items. <code>grantReadData</code> lets a Lambda read items. Using separate grants for each function means the <code>getVendors</code> Lambda can never accidentally delete data.</p>
<p><code>CfnOutput</code> prints a value to your terminal after <code>cdk deploy</code> completes. You'll use the <code>ApiEndpoint</code> URL to configure your frontend.</p>
<h2 id="heading-part-6-deploy-the-backend-to-aws">Part 6: Deploy the Backend to AWS</h2>
<p>Your infrastructure is fully defined in code. Now you'll deploy it to AWS and get a live API URL.</p>
<h3 id="heading-61-bootstrap-your-aws-environment">6.1 Bootstrap Your AWS Environment</h3>
<p>Before your first CDK deployment, AWS needs a small landing zone in your account – an S3 bucket where CDK can upload your Lambda bundles and other assets. This setup step is called <strong>bootstrapping</strong> and only needs to be done once per AWS account per region.</p>
<p>From inside your <code>backend</code> folder, run:</p>
<pre><code class="language-shell">cdk bootstrap
</code></pre>
<p><strong>Important</strong>: Bootstrapping is region-specific. If you ever switch to a different AWS region, you will need to run <code>cdk bootstrap</code> again in that region.</p>
<h3 id="heading-62-deploy">6.2 Deploy</h3>
<p>Run:</p>
<pre><code class="language-shell">cdk deploy
</code></pre>
<p>CDK will display a summary of everything it is about to create and ask for your confirmation. Type <code>y</code> and press Enter.</p>
<p>When the deployment finishes, you'll see an <strong>Outputs</strong> section in your terminal:</p>
<pre><code class="language-plaintext">Outputs:
BackendStack.ApiEndpoint = https://abcdef123.execute-api.us-east-1.amazonaws.com/prod/
</code></pre>
<p>Copy that URL. You'll need it when building the frontend.</p>
<h3 id="heading-63-troubleshooting-how-to-read-aws-error-logs">6.3 Troubleshooting: How to Read AWS Error Logs</h3>
<p>Real deployments rarely go perfectly the first time. If something goes wrong after deploying, here is how to find the actual error message.</p>
<h4 id="heading-error-502-bad-gateway">Error: 502 Bad Gateway</h4>
<p>A <code>502</code> means API Gateway received your request but your Lambda crashed before it could respond. The most common cause is a missing environment variable – for example, if <code>TABLE_NAME</code> was not passed correctly and the Lambda cannot find the table.</p>
<p>To find the actual error message, use <strong>CloudWatch Logs</strong>:</p>
<ol>
<li><p>Log in to the AWS Console and search for CloudWatch</p>
</li>
<li><p>In the left sidebar, click Logs --&gt; Log groups</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/abfb78fc-574b-4a75-a12b-12fb09f041b3.png" alt="CloudWatch left sidebar with log groups, and the search field showing /aws/lambda/" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<ol>
<li><p>Find the group named <code>/aws/lambda/BackendStack-CreateVendorHandler...</code></p>
</li>
<li><p>Click the most recent Log stream</p>
</li>
<li><p>Read the error message. It will tell you exactly what went wrong</p>
</li>
</ol>
<p>Two common messages and their fixes:</p>
<ul>
<li><p><code>Runtime.ImportModuleError</code> : Your Lambda cannot find a module. Make sure you're using <code>NodejsFunction</code> (not <code>lambda.Function</code>) in your CDK stack. <code>NodejsFunction</code> automatically bundles dependencies; <code>lambda.Function</code> does not.</p>
</li>
<li><p><code>AccessDeniedException</code>: Your Lambda tried to access DynamoDB but doesn't have permission. Check that you have the correct <code>grantWriteData</code> or <code>grantReadData</code> call in your stack for that Lambda.</p>
</li>
</ul>
<h2 id="heading-part-7-build-the-react-frontend">Part 7: Build the React Frontend</h2>
<p>Your backend is live. Now you'll build the React UI that talks to it.</p>
<h3 id="heading-71-define-the-vendor-type">7.1 Define the Vendor Type</h3>
<p>Before writing any API or component code, define what a "vendor" looks like in TypeScript. This gives you type safety throughout your frontend code.</p>
<p>Create <code>frontend/types/vendor.ts</code>:</p>
<pre><code class="language-typescript">export interface Vendor {
  vendorId?: string; // Optional when creating — the Lambda generates it
  name: string;
  category: string;
  contactEmail: string;
  createdAt?: string;
}
</code></pre>
<p>The <code>vendorId?</code> is marked optional with <code>?</code> because when you are <em>creating</em> a new vendor, you don't have an ID yet. The <code>createVendor</code> Lambda generates one. When you <em>read</em> vendors back from the API, <code>vendorId</code> will always be present.</p>
<h3 id="heading-72-create-the-api-service-layer">7.2 Create the API Service Layer</h3>
<p>Rather than writing <code>fetch</code> calls directly inside your React components, you'll centralize all your API logic in one file. This pattern is called a <strong>service layer</strong>. It keeps your components clean and makes it easy to update API calls in one place.</p>
<p>First, create a <code>.env.local</code> file inside your <code>frontend</code> folder to store your API URL:</p>
<pre><code class="language-bash"># frontend/.env.local
NEXT_PUBLIC_API_URL=https://abcdef123.execute-api.us-east-1.amazonaws.com/prod
</code></pre>
<p>Replace the URL with the <code>ApiEndpoint</code> value from your <code>cdk deploy</code> output. The <code>NEXT_PUBLIC_</code> prefix is required by Next.js to make an environment variable accessible in the browser.</p>
<p>You might be wondering: <strong>why not hardcode the URL</strong>? If you paste your API URL directly into your code and push it to GitHub, it becomes publicly visible. While an API URL alone does not expose your data (Cognito will protect that), it's good practice to keep URLs and secrets out of source control. Always use .env.local and add it to your .gitignore.</p>
<p>Make sure <code>.env.local</code> is in your <code>.gitignore</code>:</p>
<pre><code class="language-shell">echo ".env.local" &gt;&gt; frontend/.gitignore
</code></pre>
<p>Now create <code>frontend/lib/api.ts</code>:</p>
<pre><code class="language-typescript">import { Vendor } from '@/types/vendor';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;

export const getVendors = async (): Promise&lt;Vendor[]&gt; =&gt; {
  const response = await fetch(`${BASE_URL}/vendors`);
  if (!response.ok) throw new Error('Failed to fetch vendors');
  return response.json();
};

export const createVendor = async (vendor: Omit&lt;Vendor, 'vendorId' | 'createdAt'&gt;): Promise&lt;void&gt; =&gt; {
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(vendor),
  });
  if (!response.ok) throw new Error('Failed to create vendor');
};

export const deleteVendor = async (vendorId: string): Promise&lt;void&gt; =&gt; {
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'DELETE',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ vendorId }),
  });
  if (!response.ok) throw new Error('Failed to delete vendor');
};
</code></pre>
<p><strong>What each part does:</strong></p>
<ul>
<li><p><code>Omit&lt;Vendor, 'vendorId' | 'createdAt'&gt;</code> means the <code>createVendor</code> function accepts a vendor without an ID or timestamp (those are generated server-side).</p>
</li>
<li><p><code>if (!response.ok) throw new Error(...)</code> ensures that any HTTP error (4xx or 5xx) surfaces as a JavaScript error in your component, where you can show the user a meaningful message instead of silently failing.</p>
</li>
</ul>
<p>You'll update these functions later in Part 8 to include the Cognito auth token.</p>
<h3 id="heading-73-build-the-main-page">7.3 Build the Main Page</h3>
<p>Now create the main page component. It includes a form for adding vendors and a live list that displays all current vendors.</p>
<p>Replace the contents of <code>frontend/app/page.tsx</code> with:</p>
<pre><code class="language-typescript">'use client';

import { useState, useEffect } from 'react';
import { createVendor, getVendors, deleteVendor } from '@/lib/api';
import { Vendor } from '@/types/vendor';

export default function Home() {
  const [vendors, setVendors] = useState&lt;Vendor[]&gt;([]);
  const [form, setForm] = useState({ name: '', category: '', contactEmail: '' });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const loadVendors = async () =&gt; {
    try {
      const data = await getVendors();
      setVendors(data);
    } catch {
      setError('Failed to load vendors.');
    }
  };

  // Load vendors once when the page first renders
  useEffect(() =&gt; {
    loadVendors();
  }, []);
  // The empty [] means this runs only once. Without it, the effect would
  // run after every render, causing an infinite loop of fetch requests.

  const handleSubmit = async (e: React.FormEvent) =&gt; {
    e.preventDefault(); // Prevent the browser from reloading the page on submit
    setLoading(true);
    setError('');
    try {
      await createVendor(form);
      setForm({ name: '', category: '', contactEmail: '' }); // Reset the form
      await loadVendors(); // Refresh the list from DynamoDB
    } catch {
      setError('Failed to add vendor. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (vendorId: string) =&gt; {
    try {
      await deleteVendor(vendorId);
      await loadVendors(); // Refresh after deleting
    } catch {
      setError('Failed to delete vendor.');
    }
  };

  return (
    &lt;main className="p-10 max-w-5xl mx-auto"&gt;
      &lt;h1 className="text-3xl font-bold mb-2 text-gray-900"&gt;Vendor Tracker&lt;/h1&gt;
      &lt;p className="text-gray-500 mb-8"&gt;Manage your vendors, stored in AWS DynamoDB.&lt;/p&gt;

      {error &amp;&amp; (
        &lt;div className="mb-4 p-3 bg-red-100 text-red-700 rounded"&gt;{error}&lt;/div&gt;
      )}

      &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-10"&gt;

        {/* ── Add Vendor Form ── */}
        &lt;section&gt;
          &lt;h2 className="text-xl font-semibold mb-4 text-gray-800"&gt;Add New Vendor&lt;/h2&gt;
          &lt;form onSubmit={handleSubmit} className="space-y-4"&gt;
            &lt;input
              className="w-full p-2 border rounded text-black focus:outline-none focus:ring-2 focus:ring-orange-400"
              placeholder="Vendor Name"
              value={form.name}
              onChange={e =&gt; setForm({ ...form, name: e.target.value })}
              required
            /&gt;
            &lt;input
              className="w-full p-2 border rounded text-black focus:outline-none focus:ring-2 focus:ring-orange-400"
              placeholder="Category (e.g. SaaS, Hardware)"
              value={form.category}
              onChange={e =&gt; setForm({ ...form, category: e.target.value })}
              required
            /&gt;
            &lt;input
              className="w-full p-2 border rounded text-black focus:outline-none focus:ring-2 focus:ring-orange-400"
              placeholder="Contact Email"
              type="email"
              value={form.contactEmail}
              onChange={e =&gt; setForm({ ...form, contactEmail: e.target.value })}
              required
            /&gt;
            &lt;button
              type="submit"
              disabled={loading}
              className="w-full bg-orange-500 text-white p-2 rounded hover:bg-orange-600 disabled:bg-gray-400 transition-colors"
            &gt;
              {loading ? 'Saving...' : 'Add Vendor'}
            &lt;/button&gt;
          &lt;/form&gt;
        &lt;/section&gt;

        {/* ── Vendor List ── */}
        &lt;section&gt;
          &lt;h2 className="text-xl font-semibold mb-4 text-gray-800"&gt;
            Current Vendors ({vendors.length})
          &lt;/h2&gt;
          &lt;div className="space-y-3"&gt;
            {vendors.length === 0 ? (
              &lt;p className="text-gray-400 italic"&gt;No vendors yet. Add one using the form.&lt;/p&gt;
            ) : (
              vendors.map(v =&gt; (
                &lt;div
                  key={v.vendorId}
                  className="p-4 border rounded shadow-sm bg-white flex justify-between items-start"
                &gt;
                  &lt;div&gt;
                    &lt;p className="font-semibold text-gray-900"&gt;{v.name}&lt;/p&gt;
                    &lt;p className="text-sm text-gray-500"&gt;{v.category} · {v.contactEmail}&lt;/p&gt;
                  &lt;/div&gt;
                  &lt;button
                    onClick={() =&gt; v.vendorId &amp;&amp; handleDelete(v.vendorId)}
                    className="ml-4 text-sm text-red-500 hover:text-red-700 hover:underline"
                  &gt;
                    Delete
                  &lt;/button&gt;
                &lt;/div&gt;
              ))
            )}
          &lt;/div&gt;
        &lt;/section&gt;

      &lt;/div&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p><strong>Key points in this component:</strong></p>
<ul>
<li><p><code>'use client'</code> at the top is a Next.js directive. It tells Next.js that this component uses browser APIs (<code>useState</code>, <code>useEffect</code>, event handlers) and must run in the browser, not be pre-rendered on the server.</p>
</li>
<li><p><code>e.preventDefault()</code> inside <code>handleSubmit</code> stops the browser's default form submission behavior, which would cause a full page reload and wipe your React state.</p>
</li>
<li><p>After every <code>createVendor</code> or <code>deleteVendor</code> call, <code>loadVendors()</code> is called again. This re-fetches the latest data from DynamoDB so the UI always matches what is actually stored in the database.</p>
</li>
</ul>
<h3 id="heading-74-test-the-app-locally">7.4 Test the App Locally</h3>
<p>Start your Next.js development server:</p>
<pre><code class="language-shell">cd frontend
npm run dev
</code></pre>
<p>Open <code>http://localhost:3000</code> in your browser. You should see the two-panel layout. Try adding a vendor and confirm it appears in the list.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/281f971a-27b8-49b3-9079-e12601525d80.png" alt="The running Vendor Tracker app at localhost:3000 showing the two-panel layout with the Add Vendor form on the left and an empty vendor list on the right" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/88b5dd74-5847-4310-bec3-b1a2b129fbaa.png" alt="The Vendor Tracker app after a vendor has been added, showing the vendor card in the list" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h4 id="heading-verifying-the-connection-to-aws">Verifying the connection to AWS:</h4>
<p>Open Chrome DevTools (F12) and click the Network tab. When you add a vendor, you should see:</p>
<ul>
<li><p>A <code>POST</code> request to your AWS API URL returning a <strong>201</strong> status code</p>
</li>
<li><p>A <code>GET</code> request returning <strong>200</strong> with the updated vendor list</p>
</li>
</ul>
<p>You can also verify the data was saved by opening the AWS Console, navigating to <strong>DynamoDB --&gt; Tables --&gt; VendorTable --&gt; Explore table items</strong>. Your vendor should appear there.</p>
<h2 id="heading-part-8-add-authentication-with-amazon-cognito">Part 8: Add Authentication with Amazon Cognito</h2>
<p>Right now your API is completely open. Anyone who finds your API URL can add or delete vendors. You'll fix that with <strong>Amazon Cognito</strong>.</p>
<p>Cognito is AWS's authentication service. It manages a User Pool – a database of registered users with usernames and passwords. When a user logs in, Cognito issues a JWT (JSON Web Token): a cryptographically signed string that proves who the user is. Your API Gateway will check for this token on every request. No valid token means no access.</p>
<p><strong>What is a JWT?</strong> A JSON Web Token is a string that looks like <code>eyJhbGci...</code>. It contains encoded information about the user and is signed by Cognito using a secret key.</p>
<p>API Gateway can verify the signature without contacting Cognito on every request, which makes token checking fast. Think of it as a tamper-proof badge: anyone can read the name on it, but only Cognito's signature makes it valid.</p>
<h3 id="heading-81-add-cognito-to-the-cdk-stack">8.1 Add Cognito to the CDK Stack</h3>
<p>Open <code>backend/lib/backend-stack.ts</code> and update it to include Cognito. Here is the complete updated file:</p>
<pre><code class="language-typescript">import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ─── 1. DynamoDB Table ────────────────────────────────────────────────────
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // ─── 2. Lambda Functions ──────────────────────────────────────────────────
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // ─── 3. Permissions ───────────────────────────────────────────────────────
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // ─── 4. Cognito User Pool ─────────────────────────────────────────────────
    const userPool = new cognito.UserPool(this, 'VendorUserPool', {
      selfSignUpEnabled: true,
      signInAliases: { email: true },
      autoVerify: { email: true },
      userVerification: {
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
    });

    // Required to host Cognito's internal auth endpoints
    userPool.addDomain('VendorUserPoolDomain', {
      cognitoDomain: {
        domainPrefix: `vendor-tracker-${this.account}`,
      },
    });

    const userPoolClient = userPool.addClient('VendorAppClient');

    // ─── 5. API Gateway + Authorizer ──────────────────────────────────────────
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
      this,
      'VendorAuthorizer',
      { cognitoUserPools: [userPool] }
    );

    const authOptions = {
      authorizer,
      authorizationType: apigateway.AuthorizationType.COGNITO,
    };

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);

    // ─── 6. Outputs ───────────────────────────────────────────────────────────
    new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
    new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
    new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/c5e91abf-e6af-429f-bf5b-b14d18233f6c.png" alt="The newly created User Pool (VendorUserPool...) in the User Pools list, with the User Pool ID visible" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><strong>What changed:</strong></p>
<ul>
<li><p><code>CognitoUserPoolsAuthorizer</code> tells API Gateway to check every request for a valid Cognito JWT before passing it to any Lambda. If the token is missing or invalid, API Gateway rejects the request with a <code>401 Unauthorized</code> response without ever touching your Lambda.</p>
</li>
<li><p><code>authOptions</code> is applied to all three API methods: GET, POST, and DELETE. All routes are now protected.</p>
</li>
<li><p><code>autoVerify: { email: true }</code> tells Cognito to mark the email attribute as verified after a user confirms via the verification code email. It doesn't skip the verification email, as users still receive a code. If you want to skip verification during development, you can manually confirm users in the Cognito console (covered in section 8.5).</p>
</li>
<li><p>Two new <code>CfnOutput</code> values (<code>UserPoolId</code> and <code>UserPoolClientId</code>) will appear in your terminal after the next deployment. Your frontend needs them to connect to Cognito.</p>
</li>
</ul>
<p>Deploy the updated stack:</p>
<pre><code class="language-shell">cd backend
cdk deploy
</code></pre>
<p>After deployment, your terminal output will include three values:</p>
<pre><code class="language-plaintext">Outputs:
BackendStack.ApiEndpoint     = https://abc123.execute-api.us-east-1.amazonaws.com/prod/
BackendStack.UserPoolId      = us-east-1_xxxxxxxx
BackendStack.UserPoolClientId = xxxxxxxxxxxxxxxxxxxx
</code></pre>
<p>Save all three values. You'll use them in the next step.</p>
<h3 id="heading-82-install-and-configure-aws-amplify">8.2 Install and Configure AWS Amplify</h3>
<p><strong>AWS Amplify</strong> is a frontend library that handles all the complex authentication logic for you: it manages the login UI, stores tokens in the browser, refreshes expired tokens automatically, and exposes a simple API to read the current user's session.</p>
<p>Install the Amplify libraries inside your <code>frontend</code> folder:</p>
<pre><code class="language-shell">cd frontend
npm install aws-amplify @aws-amplify/ui-react
</code></pre>
<p>Create <code>frontend/app/providers.tsx</code>. This file initializes Amplify with your Cognito configuration. It runs once when the app loads:</p>
<pre><code class="language-typescript">'use client';

import { Amplify } from 'aws-amplify';

Amplify.configure(
  {
    Auth: {
      Cognito: {
        userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
        userPoolClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID!,
      },
    },
  },
  { ssr: true }
);

export function Providers({ children }: { children: React.ReactNode }) {
  return &lt;&gt;{children}&lt;/&gt;;
}
</code></pre>
<p>Add the Cognito IDs to your <code>frontend/.env.local</code> file:</p>
<pre><code class="language-shell">NEXT_PUBLIC_API_URL=https://abc123.execute-api.us-east-1.amazonaws.com/prod
NEXT_PUBLIC_USER_POOL_ID=us-east-1_xxxxxxxx
NEXT_PUBLIC_USER_POOL_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx
</code></pre>
<p>Replace the values with the outputs from your <code>cdk deploy</code>.</p>
<h3 id="heading-83-wire-providers-into-the-app-layout">8.3 Wire Providers into the App Layout</h3>
<p><strong>This step is critical.</strong> Amplify must be initialized before any component tries to use authentication. If you skip this step, <code>fetchAuthSession()</code> will throw an "Amplify not configured" error and nothing will work.</p>
<p>Open <code>frontend/app/layout.tsx</code> and update it to wrap the app in the <code>Providers</code> component:</p>
<pre><code class="language-typescript">import type { Metadata } from 'next';
import './globals.css';
import { Providers } from './providers';

export const metadata: Metadata = {
  title: 'Vendor Tracker',
  description: 'Manage your vendors with AWS',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="en"&gt;
      &lt;body&gt;
        &lt;Providers&gt;{children}&lt;/Providers&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p>By wrapping <code>{children}</code> in <code>&lt;Providers&gt;</code>, you ensure that Amplify is configured once at the root of the app, before any child page or component renders.</p>
<h3 id="heading-84-protect-the-ui-with-withauthenticator">8.4 Protect the UI with withAuthenticator</h3>
<p>Now wrap your <code>Home</code> component so that unauthenticated users see a login screen instead of the dashboard.</p>
<p>Replace the contents of <code>frontend/app/page.tsx</code> with this updated version:</p>
<pre><code class="language-typescript">'use client';

import { useState, useEffect } from 'react';
import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { getVendors, createVendor, deleteVendor } from '@/lib/api';
import { Vendor } from '@/types/vendor';

// withAuthenticator injects `signOut` and `user` as props automatically
function Home({ signOut, user }: { signOut?: () =&gt; void; user?: any }) {
  const [vendors, setVendors] = useState&lt;Vendor[]&gt;([]);
  const [form, setForm] = useState({ name: '', category: '', contactEmail: '' });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const loadVendors = async () =&gt; {
    try {
      const data = await getVendors();
      setVendors(data);
    } catch {
      setError('Failed to load vendors.');
    }
  };

  useEffect(() =&gt; {
    loadVendors();
  }, []);

  const handleSubmit = async (e: React.FormEvent) =&gt; {
    e.preventDefault();
    setLoading(true);
    setError('');
    try {
      await createVendor(form);
      setForm({ name: '', category: '', contactEmail: '' });
      await loadVendors();
    } catch {
      setError('Failed to add vendor.');
    } finally {
      setLoading(false);
    }
  };

  const handleDelete = async (vendorId: string) =&gt; {
    try {
      await deleteVendor(vendorId);
      await loadVendors();
    } catch {
      setError('Failed to delete vendor.');
    }
  };

  return (
    &lt;main className="p-10 max-w-5xl mx-auto"&gt;
      {/* ── Header ── */}
      &lt;header className="flex justify-between items-center mb-8 p-4 bg-gray-100 rounded"&gt;
        &lt;div&gt;
          &lt;h1 className="text-xl font-bold text-gray-900"&gt;Vendor Tracker&lt;/h1&gt;
          &lt;p className="text-sm text-gray-500"&gt;Signed in as: {user?.signInDetails?.loginId}&lt;/p&gt;
        &lt;/div&gt;
        &lt;button
          onClick={signOut}
          className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
        &gt;
          Sign Out
        &lt;/button&gt;
      &lt;/header&gt;

      {error &amp;&amp; (
        &lt;div className="mb-4 p-3 bg-red-100 text-red-700 rounded"&gt;{error}&lt;/div&gt;
      )}

      &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-10"&gt;

        {/* ── Add Vendor Form ── */}
        &lt;section&gt;
          &lt;h2 className="text-xl font-semibold mb-4 text-gray-800"&gt;Add New Vendor&lt;/h2&gt;
          &lt;form onSubmit={handleSubmit} className="space-y-4"&gt;
            &lt;input
              className="w-full p-2 border rounded text-black"
              placeholder="Vendor Name"
              value={form.name}
              onChange={e =&gt; setForm({ ...form, name: e.target.value })}
              required
            /&gt;
            &lt;input
              className="w-full p-2 border rounded text-black"
              placeholder="Category (e.g. SaaS, Hardware)"
              value={form.category}
              onChange={e =&gt; setForm({ ...form, category: e.target.value })}
              required
            /&gt;
            &lt;input
              className="w-full p-2 border rounded text-black"
              placeholder="Contact Email"
              type="email"
              value={form.contactEmail}
              onChange={e =&gt; setForm({ ...form, contactEmail: e.target.value })}
              required
            /&gt;
            &lt;button
              type="submit"
              disabled={loading}
              className="w-full bg-orange-500 text-white p-2 rounded hover:bg-orange-600 disabled:bg-gray-400"
            &gt;
              {loading ? 'Saving...' : 'Add Vendor'}
            &lt;/button&gt;
          &lt;/form&gt;
        &lt;/section&gt;

        {/* ── Vendor List ── */}
        &lt;section&gt;
          &lt;h2 className="text-xl font-semibold mb-4 text-gray-800"&gt;
            Current Vendors ({vendors.length})
          &lt;/h2&gt;
          &lt;div className="space-y-3"&gt;
            {vendors.length === 0 ? (
              &lt;p className="text-gray-400 italic"&gt;No vendors yet.&lt;/p&gt;
            ) : (
              vendors.map(v =&gt; (
                &lt;div
                  key={v.vendorId}
                  className="p-4 border rounded shadow-sm bg-white flex justify-between items-start"
                &gt;
                  &lt;div&gt;
                    &lt;p className="font-semibold text-gray-900"&gt;{v.name}&lt;/p&gt;
                    &lt;p className="text-sm text-gray-500"&gt;{v.category} · {v.contactEmail}&lt;/p&gt;
                  &lt;/div&gt;
                  &lt;button
                    onClick={() =&gt; v.vendorId &amp;&amp; handleDelete(v.vendorId)}
                    className="ml-4 text-sm text-red-500 hover:text-red-700 hover:underline"
                  &gt;
                    Delete
                  &lt;/button&gt;
                &lt;/div&gt;
              ))
            )}
          &lt;/div&gt;
        &lt;/section&gt;

      &lt;/div&gt;
    &lt;/main&gt;
  );
}

// Wrapping Home with withAuthenticator means any user who is not logged in
// will see Amplify's built-in login/signup screen instead of this component.
export default withAuthenticator(Home);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/e65a88dc-ea75-4daa-b7cf-eac3406c8060.png" alt="Amplify-generated login screen" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-85-pass-the-auth-token-to-api-calls">8.5 Pass the Auth Token to API Calls</h3>
<p>Now that API Gateway requires a JWT on every request, your <code>fetch</code> calls need to include the token in the <code>Authorization</code> header. Without it, every request will return a <code>401 Unauthorized</code> error.</p>
<p>Update <code>frontend/lib/api.ts</code> with a token helper and updated fetch calls:</p>
<pre><code class="language-typescript">import { fetchAuthSession } from 'aws-amplify/auth';
import { Vendor } from '@/types/vendor';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;

// Retrieves the current user's JWT token from the active Amplify session
const getAuthToken = async (): Promise&lt;string&gt; =&gt; {
  const session = await fetchAuthSession();
  const token = session.tokens?.idToken?.toString();
  if (!token) throw new Error('No active session. Please sign in.');
  return token;
};

export const getVendors = async (): Promise&lt;Vendor[]&gt; =&gt; {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    headers: { Authorization: token },
  });
  if (!response.ok) throw new Error('Failed to fetch vendors');
  return response.json();
};

export const createVendor = async (
  vendor: Omit&lt;Vendor, 'vendorId' | 'createdAt'&gt;
): Promise&lt;void&gt; =&gt; {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
    body: JSON.stringify(vendor),
  });
  if (!response.ok) throw new Error('Failed to create vendor');
};

export const deleteVendor = async (vendorId: string): Promise&lt;void&gt; =&gt; {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'DELETE',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
    body: JSON.stringify({ vendorId }),
  });
  if (!response.ok) throw new Error('Failed to delete vendor');
};
</code></pre>
<p><strong>What</strong> <code>getAuthToken</code> <strong>does:</strong></p>
<p><code>fetchAuthSession()</code> reads the currently logged-in user's session from the browser. Amplify stores the session in memory and <code>localStorage</code> after the user signs in.</p>
<p><code>session.tokens?.idToken</code> is the JWT string that API Gateway's Cognito Authorizer is looking for. Passing it as the <code>Authorization</code> header tells API Gateway: "This request is from an authenticated user."</p>
<h3 id="heading-86-troubleshooting-cognito">8.6 Troubleshooting Cognito</h3>
<h4 id="heading-unconfirmed-user-error-after-sign-up">"Unconfirmed" user error after sign-up</h4>
<p>When a new user signs up through the Amplify UI, Cognito marks the account as <em>Unconfirmed</em> until the user verifies their email address. A verification code is sent to the user's email. After entering the code, the account becomes confirmed and the user can log in.</p>
<p>If you are testing locally and want to skip the email step, you can manually confirm any account in the AWS Console:</p>
<ol>
<li><p>Open the AWS Console and navigate to Cognito</p>
</li>
<li><p>Click on your User Pool (<code>VendorUserPool...</code>)</p>
</li>
<li><p>Click the Users tab</p>
</li>
<li><p>Click on the user's email address</p>
</li>
<li><p>Open the Actions dropdown and click Confirm account</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/158fb773-9cb1-4c14-9fd7-49e4369ba7e3.png" alt=" Cognito Users list showing a user with &quot;Unconfirmed&quot; status" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/5637ac80-ee0c-4fdf-93cf-d4b7d71f6a65.png" alt="Cognito Users list showing a user with &quot;Unconfirmed&quot; status" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h4 id="heading-401-unauthorized-errors-after-deployment">401 Unauthorized errors after deployment</h4>
<p>If you are getting 401 errors, check two things:</p>
<ol>
<li><p>Open Chrome DevTools --&gt; Network tab, click the failing request, and look at the <strong>Request Headers</strong>. You should see an <code>Authorization</code> header with a long string of characters. If it is missing, <code>getAuthToken</code> is failing. Check that Amplify is configured correctly in <code>providers.tsx</code> and wired in via <code>layout.tsx</code>.</p>
</li>
<li><p>In your CDK stack, confirm that <code>authorizationType: apigateway.AuthorizationType.COGNITO</code> is present on every protected method definition. If it is missing, API Gateway may not be checking tokens even though the authorizer is defined.</p>
</li>
</ol>
<h2 id="heading-part-9-deploy-the-frontend-with-s3-and-cloudfront">Part 9: Deploy the Frontend with S3 and CloudFront</h2>
<p>Your app works locally. Now you'll deploy it to a real HTTPS URL that anyone in the world can visit.</p>
<p><strong>The strategy:</strong> Next.js will export your React app as a set of static HTML, CSS, and JavaScript files. Those files will be uploaded to an <strong>S3 bucket</strong> (AWS's file storage service). <strong>CloudFront</strong> sits in front of the bucket as a Content Delivery Network (CDN), distributing your files to servers around the world and serving them over HTTPS.</p>
<h3 id="heading-91-configure-nextjs-for-static-export">9.1 Configure Next.js for Static Export</h3>
<p>Open <code>frontend/next.config.js</code> (or <code>next.config.mjs</code>) and add the <code>output: 'export'</code> setting:</p>
<pre><code class="language-javascript">/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Generates a static /out folder instead of a Node.js server
};

export default nextConfig;
</code></pre>
<p><strong>Note on 'use client' and static export</strong>: When output: 'export' is set, Next.js builds every page at compile time. Any component that uses browser-only APIs – like withAuthenticator from Amplify – must have 'use client' at the top of the file. This tells Next.js to skip server-side rendering for that component and run it only in the browser.</p>
<p>You already have 'use client' in page.tsx. If you ever see a build error mentioning window is not defined or similar, check that the relevant component has 'use client' at the top.</p>
<p>Build the frontend:</p>
<pre><code class="language-shell">cd frontend
npm run build
</code></pre>
<p>This generates an <code>/out</code> folder containing your complete website as static files. Verify the folder was created:</p>
<pre><code class="language-shell">ls out
# You should see: index.html, _next/, etc.
</code></pre>
<h3 id="heading-92-add-s3-and-cloudfront-to-the-cdk-stack">9.2 Add S3 and CloudFront to the CDK Stack</h3>
<p>Open <code>backend/lib/backend-stack.ts</code> and add the hosting infrastructure. Here's the complete final version of the file:</p>
<pre><code class="language-typescript">import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. DynamoDB Table 
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // 2. Lambda Functions
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // 3. Permissions
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // 4. Cognito User Pool
    const userPool = new cognito.UserPool(this, 'VendorUserPool', {
      selfSignUpEnabled: true,
      signInAliases: { email: true },
      autoVerify: { email: true },
      userVerification: {
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
    });

    userPool.addDomain('VendorUserPoolDomain', {
      cognitoDomain: { domainPrefix: `vendor-tracker-${this.account}` },
    });

    const userPoolClient = userPool.addClient('VendorAppClient');

    // 5. API Gateway + Authorizer
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
      this,
      'VendorAuthorizer',
      { cognitoUserPools: [userPool] }
    );

    const authOptions = {
      authorizer,
      authorizationType: apigateway.AuthorizationType.COGNITO,
    };

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);

    // 6. S3 Bucket (Frontend Files) 
    const siteBucket = new s3.Bucket(this, 'VendorSiteBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // 7. CloudFront Distribution (HTTPS + CDN)
    const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
      defaultBehavior: {
        origin: new origins.S3Origin(siteBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      defaultRootObject: 'index.html',
      errorResponses: [
        {
          // Redirect all 404s back to index.html so React can handle routing
          httpStatus: 404,
          responseHttpStatus: 200,
          responsePagePath: '/index.html',
        },
      ],
    });

    // 8. Deploy Frontend Files to S3 
    new s3deploy.BucketDeployment(this, 'DeployWebsite', {
      sources: [s3deploy.Source.asset('../frontend/out')],
      destinationBucket: siteBucket,
      distribution,
      distributionPaths: ['/*'], // Clears CloudFront cache on every deploy
    });

    // 9. Outputs ───────────────────────────────────────────────────────────
    new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
    new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
    new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
    new cdk.CfnOutput(this, 'CloudFrontURL', {
      value: `https://${distribution.distributionDomainName}`,
    });
  }
}
</code></pre>
<p><strong>What the hosting infrastructure does:</strong></p>
<ul>
<li><p>The <strong>S3 bucket</strong> stores your static HTML, CSS, and JavaScript files. It is private – users cannot access it directly.</p>
</li>
<li><p><strong>CloudFront</strong> is the CDN that sits in front of S3. It gives you an HTTPS URL and caches your files at edge locations worldwide, so the app loads fast no matter where users are located. <code>REDIRECT_TO_HTTPS</code> automatically upgrades any HTTP request to HTTPS.</p>
</li>
<li><p>The <strong>error response</strong> for 404 returns <code>index.html</code> instead of an error page. This is necessary for single-page apps: if a user navigates directly to a route like <code>/vendors/123</code>, CloudFront cannot find a file at that path, but sending back <code>index.html</code> lets the React app handle the routing correctly.</p>
</li>
<li><p><code>distributionPaths: ['/*']</code> tells CloudFront to invalidate its entire cache after every deployment. This ensures users always see the latest version of your app immediately.</p>
</li>
<li><p><code>BucketDeployment</code> is a CDK construct that automatically uploads the contents of your <code>frontend/out</code> folder to the S3 bucket every time you run <code>cdk deploy</code>.</p>
</li>
</ul>
<h3 id="heading-93-run-the-final-deployment">9.3 Run the Final Deployment</h3>
<p>First, build the frontend with the latest environment variables:</p>
<pre><code class="language-shell">cd frontend
npm run build
</code></pre>
<p>Then deploy everything from the backend folder:</p>
<pre><code class="language-shell">cd ../backend
cdk deploy
</code></pre>
<p>After deployment finishes, copy the <code>CloudFrontURL</code> from the terminal output:</p>
<pre><code class="language-plaintext">Outputs:
BackendStack.CloudFrontURL = https://d1234abcd.cloudfront.net
</code></pre>
<p>Open that URL in your browser. Your app is now live on the internet, served over HTTPS, globally distributed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/f8e14979-a667-4afc-bdd4-9afe4abd9593.png" alt="f8e14979-a667-4afc-bdd4-9afe4abd9593" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-what-you-built">What You Built</h2>
<p>You now have a fully deployed, production-style full-stack application. Here is a summary of every piece you built and what it does:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Service</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td>Frontend</td>
<td>Next.js + CloudFront</td>
<td>React UI served globally over HTTPS</td>
</tr>
<tr>
<td>Auth</td>
<td>Amazon Cognito + Amplify</td>
<td>User sign-up, login, and JWT token management</td>
</tr>
<tr>
<td>API</td>
<td>API Gateway</td>
<td>Routes HTTP requests, validates auth tokens</td>
</tr>
<tr>
<td>Logic</td>
<td>AWS Lambda (×3)</td>
<td>Creates, reads, and deletes vendors on demand</td>
</tr>
<tr>
<td>Database</td>
<td>DynamoDB</td>
<td>Stores vendor records with no idle cost</td>
</tr>
<tr>
<td>Storage</td>
<td>S3</td>
<td>Holds your built frontend files</td>
</tr>
<tr>
<td>Infrastructure</td>
<td>AWS CDK</td>
<td>Defines and deploys all of the above as code</td>
</tr>
</tbody></table>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You have built and deployed the foundational pattern of almost every cloud application: a secured API backed by a database, deployed with infrastructure as code. Here is everything you accomplished:</p>
<p>You set up a professional AWS development environment with scoped IAM credentials. You defined your entire backend infrastructure as TypeScript code using AWS CDK, which means your database, API, Lambda functions, and authentication system are all version-controlled, repeatable, and deployable with a single command.</p>
<p>You wrote three Lambda functions that handle create, read, and delete operations, each with proper error handling and the correct AWS SDK v3 patterns. You connected them to a REST API through API Gateway and protected every route with Amazon Cognito authentication, so only registered, verified users can interact with your data.</p>
<p>On the frontend, you built a Next.js application with a service layer that cleanly separates API logic from UI components, manages JWTs automatically through AWS Amplify, and gives users a complete sign-up and sign-in flow without you writing a single line of authentication UI code.</p>
<p>Finally, you deployed the entire system: your backend to AWS Lambda and DynamoDB, and your frontend as a static site served globally through CloudFront over HTTPS.</p>
<p>The full source code for this tutorial is available on <a href="https://github.com/BenedictaUche/vendor-tracker">GitHub</a>. Clone it, modify it, and use it as a reference for your own projects.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Serverless RAG Pipeline on AWS That Scales to Zero ]]>
                </title>
                <description>
                    <![CDATA[ Most RAG tutorials end the same way: you've got a working prototype and a bill for a vector database that runs whether anyone's querying it or not. Add an always-on embedding service, a hosted LLM end ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-serverless-rag-pipeline-on-aws-that-scales-to-zero/</link>
                <guid isPermaLink="false">69b1b23c6c896b0519b4eda8</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RAG  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Christopher Galliart ]]>
                </dc:creator>
                <pubDate>Wed, 11 Mar 2026 18:19:40 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c0416d9e-9661-47a3-ba9c-8001f5f91b8c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most RAG tutorials end the same way: you've got a working prototype and a bill for a vector database that runs whether anyone's querying it or not. Add an always-on embedding service, a hosted LLM endpoint, and the usual AWS infrastructure, and you're looking at real money before a single user shows up.</p>
<p>But it doesn't have to work that way. In this tutorial, you'll deploy a fully serverless RAG pipeline that processes documents, images, video, and audio, then scales to zero when nobody's using it.</p>
<p>Everything runs in your AWS account, your data never leaves your infrastructure, and your ongoing monthly cost for a modest knowledge base will be closer to <code>2-3 USD</code> than <code>300 USD</code>.</p>
<p>We'll use <a href="https://github.com/HatmanStack/RAGStack-Lambda">RAGStack-Lambda</a>, an open-source project I built on AWS. By the end, you'll have a deployed pipeline with a dashboard, an AI chat interface with source citations, a drop-in web component you can embed in any app, and an MCP server you can use to feed your assistant context.</p>
<h3 id="heading-heres-what-well-cover">Here's what we'll cover:</h3>
<ul>
<li><p><a href="#heading-what-this-actually-costs">What This Actually Costs</a></p>
</li>
<li><p><a href="#heading-what-youre-building">What You're Building</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-deploying-from-aws-marketplace">Deploying from AWS Marketplace</a></p>
</li>
<li><p><a href="#heading-deploying-from-source">Deploying from Source</a></p>
</li>
<li><p><a href="#heading-uploading-your-first-documents">Uploading Your First Documents</a></p>
</li>
<li><p><a href="#heading-chatting-with-your-knowledge-base">Chatting With Your Knowledge Base</a></p>
</li>
<li><p><a href="#heading-embedding-the-web-component-in-your-app">Embedding the Web Component in Your App</a></p>
</li>
<li><p><a href="#heading-using-the-mcp-server">Using the MCP Server</a></p>
</li>
<li><p><a href="#heading-what-you-can-build-from-here">What You Can Build From Here</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-what-this-actually-costs">What This Actually Costs</h2>
<p>Before we build anything, let's talk money, because the cost story is the whole point.</p>
<p>RAG pipelines have two cost phases: ingestion (processing your documents once) and operation (querying them over time).</p>
<p>Most platforms charge you a flat monthly rate regardless of which phase you're in. A serverless architecture flips that: ingestion costs something, and then everything scales to zero.</p>
<h3 id="heading-ingestion-the-one-time-hit">Ingestion: The One-Time Hit</h3>
<p>When you upload documents, several things happen: text extraction (OCR for PDFs and images), embedding generation, metadata extraction, and storage. Here's what that actually costs per service:</p>
<p><strong>Textract (OCR):</strong> This is the most expensive part of ingestion, and it only applies to scanned PDFs and images that need text extraction. Plain text, HTML, CSV, and other text-based formats skip this entirely.</p>
<p>Textract charges about <code>1.50 USD</code> per 1,000 pages for standard text detection. If you're uploading 500 pages of scanned PDFs, that's about <code>0.75 USD</code>. A heavy initial load of several thousand scanned pages might run <code>5-10 USD</code>. But once your documents are processed, you never pay this again unless you add new ones.</p>
<p><strong>Bedrock Embeddings (Nova Multimodal):</strong> This is where your content gets converted into vectors for semantic search. The pricing is almost comically cheap:</p>
<ul>
<li><p>Text: <code>0.00002 USD</code> per 1,000 input tokens</p>
</li>
<li><p>Images: <code>0.00115 USD</code> per image</p>
</li>
<li><p>Video/Audio: <code>0.00200 USD</code> per minute</p>
</li>
</ul>
<p>To put that in perspective: if you have 1,500 text documents averaging 2,500 tokens each after chunking, your total embedding cost is about <code>0.08 USD</code>. A knowledge base with 500 images runs <code>0.58 USD</code>. Even a mixed corpus of text, images, and a few hours of video stays well under <code>2 USD</code> for the entire embedding pass. This is a one-time cost – you only re-embed if you add or update documents.</p>
<p><strong>Bedrock LLM (Metadata Extraction):</strong> RAGStack uses an LLM to analyze each document and extract structured metadata automatically. This is a few inference calls per document using Nova Lite or a similar model. At <code>0.06 USD</code>/<code>0.24 USD</code> per million input/output tokens, processing 1,500 documents costs well under <code>1 USD</code>.</p>
<p><strong>S3 Vectors (Storage):</strong> Storing your embeddings. At <code>0.06 USD</code> per GB/month, a knowledge base of 1,500 documents with 1,024-dimension vectors takes up a trivially small amount of space. We're talking pennies per month.</p>
<p><strong>S3 (Document Storage):</strong> Your source documents in standard S3. Even cheaper, <code>0.023 USD</code> per GB/month.</p>
<p><strong>DynamoDB:</strong> Stores document metadata and processing state. The on-demand pricing model means you pay per request during ingestion, then essentially nothing at rest. A few cents for the initial load.</p>
<p>To put real numbers on it: if you upload 200 text documents (PDFs, HTML, markdown), your total ingestion cost is likely under <code>1 USD</code>. If you upload 1,000 scanned PDFs that need OCR, you might see <code>5-8 USD</code> as a one-time hit. That <code>7-10 USD</code> figure you might see referenced? That's the upper end for a heavy initial load with lots of OCR work.</p>
<h3 id="heading-operation-where-scale-to-zero-shines">Operation: Where Scale-to-Zero Shines</h3>
<p>Once your documents are ingested, the pipeline is waiting. Not running. Waiting. Here's what each query costs:</p>
<p><strong>Lambda:</strong> Invocations are billed per request and duration. The free tier covers 1 million requests/month. For a personal or small-team knowledge base, you may never leave the free tier.</p>
<p><strong>S3 Vectors (Queries):</strong> <code>2.50 USD</code> per million query API calls, plus a per-TB data processing charge. For a small index queried a few hundred times a month, this rounds to effectively zero.</p>
<p><strong>Bedrock (Chat Inference):</strong> This is your main operating cost. Each chat response requires an LLM call. Using Nova Lite at <code>0.06 USD</code> per million input tokens and <code>0.24 USD</code> per million output tokens, a typical RAG query (retrieval context + user question + response) might cost <code>0.001-0.003 USD</code> per query. A hundred queries a month is <code>0.10-0.30 USD</code>.</p>
<p><strong>Step Functions:</strong> Orchestrates the document processing pipeline. Standard workflows charge <code>0.025 USD</code> per 1,000 state transitions. Minimal during operation since it's only active during ingestion.</p>
<p><strong>Cognito:</strong> User authentication. Free for the first 10,000 monthly active users.</p>
<p><strong>CloudFront:</strong> Serves the dashboard UI. Free tier covers 1 TB of data transfer per month.</p>
<p><strong>API Gateway:</strong> Handles GraphQL API requests. Free tier covers 1 million API calls per month.</p>
<p>Add it all up for a knowledge base with 500 documents getting a few hundred queries per month, and your monthly operating cost is somewhere between <code>0.50 USD</code> and <code>3.00 USD</code>. Most of that is the LLM inference for chat responses.</p>
<h3 id="heading-the-comparison-that-matters">The Comparison That Matters</h3>
<p>Here's the same pipeline on a traditional always-on stack:</p>
<table>
<thead>
<tr>
<th>Service</th>
<th>RAGStack-Lambda</th>
<th>Traditional Stack</th>
</tr>
</thead>
<tbody><tr>
<td>Vector Database</td>
<td>S3 Vectors: pennies/mo</td>
<td>Pinecone Starter: <code>70 USD</code>/mo</td>
</tr>
<tr>
<td>Vector Database (alt)</td>
<td>S3 Vectors: pennies/mo</td>
<td>OpenSearch Serverless: about <code>350 USD</code>/mo min</td>
</tr>
<tr>
<td>Compute</td>
<td>Lambda: free tier</td>
<td>EC2 or ECS: <code>50-150 USD</code>/mo</td>
</tr>
<tr>
<td>LLM Inference</td>
<td>Same per-query cost</td>
<td>Same per-query cost</td>
</tr>
<tr>
<td>Total (idle)</td>
<td>about <code>0.50-3.00 USD</code>/mo</td>
<td><code>120-500 USD</code>/mo</td>
</tr>
</tbody></table>
<p>The LLM inference cost per query is roughly the same everywhere – that's Bedrock's on-demand pricing regardless of your architecture. The difference is everything else. Traditional stacks pay a floor cost whether anyone's using them or not. A serverless stack pays for what it uses, and idle costs essentially nothing.</p>
<h3 id="heading-what-about-transcribe">What About Transcribe?</h3>
<p>If you're uploading video or audio, AWS Transcribe adds cost for speech-to-text conversion. Standard transcription runs about <code>0.024 USD</code> per minute of audio. A 10-minute video costs <code>0.24 USD</code> to transcribe. This is a one-time ingestion cost, once transcribed and embedded, the resulting text chunks are queried like any other document.</p>
<h2 id="heading-what-youre-building">What You're Building</h2>
<p>By the end of this tutorial, you'll have a deployed pipeline that does the following:</p>
<ol>
<li><p>You upload a document (PDF, image, video, audio, HTML, CSV, <a href="https://github.com/HatmanStack/RAGStack-Lambda/blob/main/docs/ARCHITECTURE.md">the full list</a> is extensive) through a web dashboard.</p>
</li>
<li><p>The pipeline detects the file type and routes it to the right processor. Scanned PDFs go through OCR via Textract. Video and audio go through Transcribe for speech-to-text, split into 30-second searchable chunks with speaker identification. Images get visual embeddings and any caption text you provide.</p>
</li>
<li><p>An LLM analyzes each document and extracts structured metadata, topic, document type, date range, people mentioned, whatever's relevant. This happens automatically.</p>
</li>
<li><p>Everything gets embedded using Amazon Nova Multimodal Embeddings and stored in a Bedrock Knowledge Base backed by S3 Vectors.</p>
</li>
<li><p>You (or your users) ask questions through an AI chat interface. The pipeline retrieves relevant documents, passes them as context to a Bedrock LLM, and returns an answer with collapsible source citations, including timestamp links for video and audio that jump to the exact position.</p>
</li>
</ol>
<p>All of this runs in your AWS account. No external control plane, no third-party services beyond AWS itself.</p>
<h3 id="heading-the-architecture">The Architecture</h3>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/45eca6a5-91b4-4f55-8b1a-ba9f59a3e25d.png" alt="The diagram illustrates a flowchart of a buyer's AWS account, detailing the application plane with processes like S3 to Lambda OCR, supported by services like Cognito Auth. It emphasizes Amazon Bedrock's integration for knowledge and chat." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>A few things to note about this architecture:</p>
<p><strong>Step Functions orchestrate everything.</strong> When a document is uploaded, a state machine manages the entire processing flow, detecting the file type, routing to the right processor, waiting for async operations like Transcribe jobs, then triggering embedding and metadata extraction.</p>
<p>This is what makes the pipeline reliable without a running server. If a step fails, it retries. You can see exactly where every document is in the processing pipeline.</p>
<p><strong>Lambda does the compute.</strong> Every processing step is a Lambda function. They spin up when needed, run for a few seconds to a few minutes, and shut down. There's no EC2 instance idling at 3 AM.</p>
<p><strong>S3 Vectors is the vector store.</strong> Your embeddings live in S3's purpose-built vector storage rather than in a dedicated vector database like Pinecone or OpenSearch.</p>
<p>This is what makes the "scale to zero" cost possible: you're paying object storage rates for vector data instead of keeping a database cluster warm. It also means your vectors are sitting in your own S3 bucket, not in a third-party managed service that holds your data on their terms.</p>
<p><strong>Cognito handles auth.</strong> The dashboard and API are protected with Cognito user pools. When you deploy, you get a temporary password via email. The web component uses IAM-based authentication, and server-side integrations use API key auth.</p>
<p><strong>CloudFront serves the UI.</strong> The dashboard is a static React app served through CloudFront, so there's no web server to maintain.</p>
<h3 id="heading-two-ways-to-deploy">Two Ways to Deploy</h3>
<p>You have two deployment paths depending on what you want:</p>
<p><strong>AWS Marketplace (the fast path)</strong>, click deploy, fill in two fields (stack name and email), and wait about 10 minutes. No local tooling required. This is the path we'll walk through first.</p>
<p><strong>From Source (the developer path)</strong>, Clone the repo, run <code>publish.py</code>, and deploy via SAM CLI. This is the path for when you want to customize the processing pipeline, modify the UI, or contribute to the project. We'll cover this after the Marketplace walkthrough.</p>
<p>Both paths produce the same stack. The Marketplace version just wraps the CloudFormation template in a one-click deployment.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you deploy, you'll need:</p>
<ul>
<li><p><strong>An AWS account</strong> with permissions to create CloudFormation stacks, Lambda functions, S3 buckets, DynamoDB tables, and Cognito user pools. If you're using an admin account, you're covered.</p>
</li>
<li><p><strong>Bedrock model access:</strong> RAGStack defaults to <code>us-east-1</code> because that's where Nova Multimodal Embeddings is available. Amazon's own models (including Nova) are available by default in Bedrock, no manual enablement required. Just make sure your IAM role has the necessary <code>bedrock:InvokeModel</code> permissions.</p>
</li>
<li><p><strong>For the Marketplace path:</strong> just a web browser.</p>
</li>
<li><p><strong>For the source path:</strong> Python 3.13+, Node.js 24+, AWS CLI and SAM CLI configured, and Docker (for building Lambda layers).</p>
</li>
</ul>
<h2 id="heading-deploying-from-aws-marketplace">Deploying from AWS Marketplace</h2>
<p>This is the fastest path – no local tools, no CLI, no Docker. You'll launch a CloudFormation stack and have a working pipeline in about 10 minutes.</p>
<h3 id="heading-step-1-launch-the-stack">Step 1: Launch the Stack</h3>
<p>Click the <a href="https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://ragstack-quicklaunch-public.s3.us-east-1.amazonaws.com/ragstack-template.yaml&amp;stackName=my-docs">direct deploy link</a> to open CloudFormation's "Quick create stack" page with the template pre-loaded.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/d354f6bc-dee8-4f44-9b3b-523ea27564c7.png" alt="Screenshot of AWS CloudFormation Quick Create Stack page in dark mode. Sections for template URL, stack name, parameters, and build options are visible." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-step-2-fill-in-two-fields">Step 2: Fill In Two Fields</h3>
<p>The page has a lot of options, but you only need two:</p>
<ul>
<li><p><strong>Stack name:</strong> Must be lowercase. This becomes the prefix for all your AWS resources (for example, <code>my-docs</code>, <code>team-kb</code>, <code>project-notes</code>). Keep it short.</p>
</li>
<li><p><strong>Admin Email:</strong> Under Required Settings. Cognito will send your temporary login credentials here. Use an email you can access right now.</p>
</li>
</ul>
<p>Everything else – Build Options, Advanced Settings, OCR Backend, model selections – can stay at the defaults. They're there for customization later, but the defaults work out of the box.</p>
<h3 id="heading-step-3-deploy">Step 3: Deploy</h3>
<p>Scroll to the bottom, check the three acknowledgment boxes under "Capabilities and transforms," and click <strong>Create stack</strong>.</p>
<p>Deployment takes roughly 10 minutes. You can watch the progress in the CloudFormation Events tab if you're curious, but there's nothing to do until the stack status flips to <code>CREATE_COMPLETE</code>.</p>
<h3 id="heading-step-4-log-in">Step 4: Log In</h3>
<p>Once the stack finishes, check your email. Cognito sends you the dashboard URL and a temporary password. Log in, set a new password, and you're looking at an empty dashboard ready for documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/5ac31b6c-2782-4b66-82a9-0cb962c5dac4.png" alt="A software dashboard interface titled 'Document Pipeline (Demo)' displaying options for uploading, scraping, and searching documents. The screen shows no current documents or scrape jobs, with menu options on the left and a search and filter bar at the center. The overall tone is functional and minimalist." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-deploying-from-source">Deploying from Source</h2>
<p>If you want to customize the pipeline, modify the UI, or contribute to the project, deploy from source instead.</p>
<h3 id="heading-step-1-clone-and-set-up">Step 1: Clone and Set Up</h3>
<pre><code class="language-bash">git clone https://github.com/HatmanStack/RAGStack-Lambda.git
cd RAGStack-Lambda

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
</code></pre>
<h3 id="heading-step-2-deploy">Step 2: Deploy</h3>
<p>The <code>publish.py</code> script handles everything: building the frontend, packaging Lambda functions, and deploying via SAM CLI.</p>
<pre><code class="language-bash">python publish.py \
  --project-name my-docs \
  --admin-email admin@example.com
</code></pre>
<p>This defaults to <code>us-east-1</code> for Nova Multimodal Embeddings. The script will build the React dashboard, build the web component, package all Lambda layers with Docker, and deploy the CloudFormation stack through SAM.</p>
<p>First deploy takes longer (15-20 minutes) because it's building everything from scratch. Subsequent deploys are faster since SAM caches unchanged resources.</p>
<p>If you only want to iterate on the backend and skip UI builds:</p>
<pre><code class="language-bash"># Skip dashboard build (still builds web component)
python publish.py --project-name my-docs --admin-email admin@example.com --skip-ui

# Skip ALL UI builds
python publish.py --project-name my-docs --admin-email admin@example.com --skip-ui-all
</code></pre>
<p>Once it finishes, you'll get the same Cognito email and dashboard URL as the Marketplace path.</p>
<h2 id="heading-uploading-your-first-documents">Uploading Your First Documents</h2>
<p>The dashboard has tabs for different content types. We'll start with the Documents tab since that's the most common use case.</p>
<h3 id="heading-documents">Documents</h3>
<p>Click the <strong>Documents</strong> tab and upload a file. RAGStack accepts a wide range of formats: PDF, DOCX, XLSX, HTML, CSV, JSON, XML, EML, EPUB, TXT, and Markdown. Drag and drop or use the file picker.</p>
<p>Once uploaded, the document enters the processing pipeline. You'll see the status update in real time:</p>
<ol>
<li><p><strong>UPLOADED:</strong> File received and stored in S3.</p>
</li>
<li><p><strong>PROCESSING:</strong> Step Functions has picked it up and routed it to the right processor. Text-based files (HTML, CSV, Markdown) go through direct extraction. Scanned PDFs and images go through Textract OCR. The LLM analyzes the content and extracts structured metadata, topic, document type, people mentioned, date ranges, whatever's relevant to the content.</p>
</li>
<li><p><strong>INDEXED:</strong> Embeddings generated, vectors stored, document is searchable.</p>
</li>
</ol>
<p>Text documents typically process in 1-5 minutes. OCR-heavy documents (scanned PDFs, images with text) can take 2-15 minutes depending on page count.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/3df05041-2632-41a9-a71c-6d764c503f2a.png" alt="Screenshot of a document upload interface labeled &quot;Document Pipeline (Demo).&quot; Central panel shows a box for drag-and-drop file upload. Sleek, modern design." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-images">Images</h3>
<p>The <strong>Images</strong> tab works differently. Upload a JPG, PNG, GIF, or WebP and you can add a caption. Both the visual content and caption text get embedded using Nova Multimodal Embeddings, so you can search by what's in the image or by your description of it.</p>
<p>This is where multimodal embeddings earn their keep. A traditional text-only RAG pipeline would need you to describe every image manually. Here, the image itself becomes searchable, and since everything stays in your AWS account, you're not sending personal photos or sensitive visual content to an external service to get there.</p>
<h3 id="heading-what-about-video-and-audio">What About Video and Audio?</h3>
<p>Upload video or audio files and RAGStack routes them through AWS Transcribe for speech-to-text conversion. The transcript gets split into 30-second chunks with speaker identification, then embedded like any other document. When chat results reference a video source, you get timestamp links that jump to the exact position in the recording.</p>
<h3 id="heading-web-scraping">Web Scraping</h3>
<p>The <strong>Scrape</strong> tab lets you pull websites directly into your knowledge base. Enter a URL and RAGStack crawls the page, extracts the content, and processes it through the same pipeline as uploaded documents, metadata extraction, embedding, indexing.</p>
<p>This is useful for building a knowledge base from existing web content without manually saving and uploading pages. Documentation sites, blog archives, reference material, anything publicly accessible.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/ac2c6239-a323-4770-80f7-31aa7ff3bdfb.png" alt="Web scraping interface with fields for URL, max pages, and depth. A dropdown for scope selection and a 'Start Scrape' button are visible." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-chatting-with-your-knowledge-base">Chatting With Your Knowledge Base</h2>
<p>This is the payoff. Go to the <strong>Chat</strong> tab, type a question, and RAGStack retrieves relevant documents from your knowledge base, passes them as context to a Bedrock LLM, and returns an answer with source citations.</p>
<p>The citations are collapsible, so click to expand and see which documents informed the answer, with the option to download the source file. For video and audio sources, you get clickable timestamps that jump to the relevant moment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/698f5932352111d3f67030a2/760b3cd0-8bb8-493d-97ce-5eb3d0138592.png" alt="Screenshot of a web interface titled &quot;Knowledge Base Chat&quot; with menu options on the left. The central section prompts users to ask document-related questions." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-metadata-filtering">Metadata Filtering</h3>
<p>If you've uploaded enough documents to have meaningful metadata categories, the chat interface lets you filter search results by metadata before querying. RAGStack auto-discovers the metadata structure from your documents, so you don't configure this manually, it just appears as your knowledge base grows.</p>
<p>This is useful when you have a large mixed corpus. Instead of hoping the vector search picks the right context from thousands of documents, you can narrow it down: "only search documents about project X" or "only search content from Q4 2024."</p>
<h2 id="heading-embedding-the-web-component-in-your-app">Embedding the Web Component in Your App</h2>
<p>The dashboard is useful for managing your knowledge base, but the real power is embedding RAGStack's chat in your own application. The web component works with any framework, React, Vue, Angular, Svelte, plain HTML.</p>
<p>Load the script once from your CloudFront distribution:</p>
<pre><code class="language-html">&lt;script src="https://your-cloudfront-url/ragstack-chat.js"&gt;&lt;/script&gt;
</code></pre>
<p>Then drop the component wherever you want a chat interface:</p>
<pre><code class="language-html">&lt;ragstack-chat
  conversation-id="my-app"
  header-text="Ask About Documents"
&gt;&lt;/ragstack-chat&gt;
</code></pre>
<p>That's it. The component handles authentication (via IAM), manages conversation state, and renders source citations, all self-contained. Your CloudFront URL is in the stack outputs.</p>
<p>For server-side integrations that don't need a UI, the GraphQL API is available with API key authentication. You can find your endpoint and API key in the dashboard under Settings.</p>
<h2 id="heading-using-the-mcp-server">Using the MCP Server</h2>
<p>RAGStack includes an MCP server that connects your knowledge base to AI assistants like Claude Desktop, Cursor, VS Code, and Amazon Q CLI. Instead of switching to the dashboard to search your documents, you ask your assistant directly.</p>
<p>Install it:</p>
<pre><code class="language-bash">pip install ragstack-mcp
</code></pre>
<p>Then add it to your AI assistant's MCP configuration:</p>
<pre><code class="language-json">{
  "ragstack": {
    "command": "uvx",
    "args": ["ragstack-mcp"],
    "env": {
      "RAGSTACK_GRAPHQL_ENDPOINT": "YOUR_ENDPOINT",
      "RAGSTACK_API_KEY": "YOUR_API_KEY"
    }
  }
}
</code></pre>
<p>Your endpoint and API key are in the dashboard under Settings. Once configured, type <code>@ragstack</code> in your assistant's chat to invoke the MCP server, then ask things like "search my knowledge base for authentication docs" and it queries RAGStack directly.</p>
<p>See the <a href="https://github.com/HatmanStack/RAGStack-Lambda/blob/main/src/ragstack-mcp/README.md">MCP Server docs</a> for the full list of available tools and setup details.</p>
<h2 id="heading-what-you-can-build-from-here">What You Can Build From Here</h2>
<p>You've got a deployed RAG pipeline that costs almost nothing to run and handles text, images, video, and audio. A few directions you might take it:</p>
<p><strong>A searchable personal archive.</strong> Every conference talk you've saved, every PDF textbook, every tutorial video that's sitting in a folder somewhere. Upload it all, and now you have one search interface across years of accumulated material. The multimodal embeddings mean your screenshots and diagrams are searchable too, not just the text.</p>
<p>I built <a href="https://github.com/HatmanStack/family-archive-document-ai">a family archive app</a> this way, scanned letters, old photos, home videos, with RAGStack deployed as a nested CloudFormation stack so the whole family can search across decades of memories using the chat widget.</p>
<p><strong>A second brain for a client project.</strong> Scrape the client's existing docs, upload the SOW and meeting notes, drop in the codebase documentation. Now you've got a searchable knowledge base scoped to that engagement. Spin it up at the start, tear it down when the contract ends. At these costs, it's disposable infrastructure.</p>
<p><strong>AI chat over a niche dataset.</strong> Recipe collections, legal filings, research papers, local government meeting minutes, any corpus that's too specialized for general-purpose LLMs to know well. The web component means you can ship it as a standalone tool without building a frontend from scratch.</p>
<p><strong>RAG for your MCP workflow.</strong> If you're already using Claude Desktop or Cursor, the MCP server turns your knowledge base into another tool your assistant can reach for. Upload your team's runbooks and architecture docs, and now <code>@ragstack</code> in your editor gives you instant context without tab-switching.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>The serverless RAG pipeline you just deployed handles document processing, multimodal embeddings, metadata extraction, and AI chat with source citations, all scaling to zero when idle, all running in your AWS account. Your documents, your vectors, your infrastructure. The traditional approach to this stack costs <code>120-500 USD</code>/month in baseline infrastructure. This one costs pocket change.</p>
<p>The full source is at <a href="https://github.com/HatmanStack/RAGStack-Lambda">github.com/HatmanStack/RAGStack-Lambda</a>. File issues, open PRs, or just poke around the architecture. If you want to go deeper on the technical tradeoffs, particularly how filtered vector search behaves on cost-optimized backends like S3 Vectors, that's a story for the next post.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Run a Docker Container in AWS Lambda ]]>
                </title>
                <description>
                    <![CDATA[ While containers are quite lightweight and provide various benefits, it can be challenging to decide how best to deploy them. There are a number of ways to deploy and run Docker containers. But some are best for orchestrating and managing containers,... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-run-a-docker-container-in-aws-lambda/</link>
                <guid isPermaLink="false">694c7990b7478745bce04604</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ lambda ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containerization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ecr ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Agnes Olorundare ]]>
                </dc:creator>
                <pubDate>Wed, 24 Dec 2025 23:38:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766599506861/86c07e37-7838-4186-971e-29722ccec785.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>While containers are quite lightweight and provide various benefits, it can be challenging to decide how best to deploy them. There are a number of ways to deploy and run Docker containers. But some are best for orchestrating and managing containers, and may not suit a simple use case of running just one container.</p>
<p>In this article, I’ll teach you how you can deploy a single Docker container using a serverless service on AWS called Lambda.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisite-requirements">Prerequisite/ Requirements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-serverless-with-aws-lambda">Serverless with AWS Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-run-and-test-a-container-locally">How to Build, Run, and Test a Container Locally</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-push-your-image-to-amazon-elastic-container-registry-ecr">How to Push Your Image to Amazon Elastic Container Registry (ECR)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-your-docker-image-to-lambda">How to Deploy Your Docker Image to Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cleanup">Cleanup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisite-requirements">Prerequisite/ Requirements</h2>
<p>The following tools and skills are necessary for following along with this tutorial:</p>
<ul>
<li><p>Knowledge of Docker, and have Docker installed locally.</p>
</li>
<li><p>An AWS account with credentials with administrative privilege for making API calls via the CLI. Best practice would be to limit the privilege to exactly what needs to be done.</p>
</li>
<li><p>AWS CLI installed locally</p>
</li>
<li><p>Python virtual environment managers <a target="_blank" href="https://github.com/astral-sh/uv">such as uv</a> (optional)</p>
</li>
</ul>
<h2 id="heading-serverless-with-aws-lambda">Serverless with AWS Lambda</h2>
<p>Containers provide a lightweight, consistent, and resource-friendly way of running applications. Serverless takes away the overhead of managing the underlying infrastructures on which the container runs. So as you can probably start to see, combining these tools helps you deploy applications in a way that lets you focus on business logic, performance, and what gives your product a competitive edge/ advantage.</p>
<p>One AWS tool that enables you to go serverless is Lambda. With Lambda, you’re only billed for the number of times the code in the function runs, the memory you selected at the time of provisioning the service, and the duration of each invocation of the function.</p>
<p>In addition to removing operational overhead, Lambda can also help you save money since you won’t have to deal with idle resources. The function only comes alive when triggered by a request sent to it.</p>
<h2 id="heading-how-to-build-run-and-test-a-container-locally">How to Build, Run, and Test a Container Locally</h2>
<p>Docker is a tool that helps you package applications or software into portable, standardized and shareable units that have everything the applications need such as libraries, runtime, system tools, application code, in order to run. These units are called containers.</p>
<p>In this section, I’ll walk you through building the Docker image, running the container, and testing it after it’s running.</p>
<p>You can find the project that you’ll be using here in this <a target="_blank" href="https://github.com/Agnes4Him/freecodecamp-lambda-docker">GitHub repository</a>.</p>
<h3 id="heading-build-the-docker-image">Build the Docker Image</h3>
<p>To run a Docker container, you first need to build an image. The image becomes the template or <code>class</code> from which you create the container or <code>instance of the class</code>.</p>
<p>You can find the code to build an image in <code>lambda_function.py</code>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># lambda_function.py</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lambda_handler</span>(<span class="hljs-params">event, context</span>):</span>
    name = event[<span class="hljs-string">"name"</span>]
    message = <span class="hljs-string">f"Hello, <span class="hljs-subst">{name}</span>!"</span>

    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"statusCode"</span>: <span class="hljs-number">200</span>,
            <span class="hljs-string">"body"</span>: message
        }
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"statusCode"</span>: <span class="hljs-number">400</span>,
            <span class="hljs-string">"body"</span>: {<span class="hljs-string">"error"</span>: str(e)}
        }
</code></pre>
<p>As you can see from the code above, this is a very basic Python application that expects a <code>POST</code> HTTP request, with a JSON payload that contains the key – <code>name</code> – and a corresponding value. The code then returns a greeting containing the name it has received. The application has just a single function, which also serves as the entry point to it.</p>
<p>To build a Docker image, you’ll need a Dockerfile to provide the blueprint for the image. For this specific case, the Dockerfile you’ll use is also very basic. Each line in a Dockerfile is called a <code>Directive</code>, and this provides the instruction Docker should follow when creating an image. So building a Docker image means creating a template for a container by following the instructions or directives in the Dockerfile.</p>
<pre><code class="lang-plaintext"># Dockerfile

FROM public.ecr.aws/lambda/python:3.12

# Copy function code... LAMBDA_TASK_ROOT is /var/task, the working directory set in the base image
COPY lambda_function.py ${LAMBDA_TASK_ROOT}    

# Set the CMD to your handler - lambda_handler
CMD ["lambda_function.lambda_handler"]
</code></pre>
<p>A Dockerfile usually starts with a base image. To deploy an application as a Docker container in AWS Lambda, the base image has to be of a specific kind, depending on the application run-time. For this case, you’ll need the Python run-time, so the base image is <code>public.ecr.aws/lambda/python:3.12</code>. It’s okay to use a different Python version.</p>
<p>The next directive in the Dockerfile is copying the <code>lambda_function.py</code> file to a specific path in the base image. That path is referenced using an environment variable that has already been defined in the base image and points to <code>/var/task</code>. This is the directory your code will be running from.</p>
<p>The last directive is simply a command to start the application when the container runs.</p>
<p>Now, you can run the build command from the project’s root directory:</p>
<pre><code class="lang-bash">docker build -t &lt;IMAGE_NAME&gt;:&lt;iIMAGE_TAG&gt; .
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766415846066/f128b7fc-f3a0-4770-b361-3f27c36a6ec4.png" alt="Running docker build command on the terminal" class="image--center mx-auto" width="3710" height="891" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766415895836/d4653144-51b2-437d-8d73-4aaa42651206.png" alt="Output of docker images command showing a list of all existing images" class="image--center mx-auto" width="3710" height="891" loading="lazy"></p>
<h3 id="heading-run-the-docker-container">Run the Docker Container</h3>
<p>Next, let’s create a running container from this image.</p>
<pre><code class="lang-bash">docker run -it --rm -p 8080:8080  lambda_docker:1.0.0
</code></pre>
<p>The command above will create a container and run it in interactive mode just so you can see the logs generated by the application in the container. Port 8080 is also exposed on the host where the container is running and mapped to the container port, which is also 8080 (defined by AWS). The container gets automatically removed once you kill the running process with CTRL + C.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766416250857/62584a3c-bf5e-4cd9-b8d5-fc6734c50075.png" alt="Showing docker run command in interactive mode" class="image--center mx-auto" width="3710" height="891" loading="lazy"></p>
<h3 id="heading-test-the-running-container">Test the Running Container</h3>
<p>Now confirm that the application running within the container can receive and process requests. To do this, use the code in the <code>test.py</code> file:</p>
<pre><code class="lang-python"><span class="hljs-comment"># test.py</span>

<span class="hljs-keyword">import</span> requests

url = <span class="hljs-string">"http://localhost:8080/2015-03-31/functions/function/invocations"</span>

data = {
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"Janet"</span>
}

response = requests.post(url, json=data)

print(<span class="hljs-string">"Status Code:"</span>, response.status_code)
print(<span class="hljs-string">"Response Body:"</span>, response.json())
</code></pre>
<p>You can use the Python <code>requests</code> library to make this call. Install the library by using a virtual environment to isolate the application from your overall system. This helps prevent issues with conflicts in the versions of libraries you install for an application to use.</p>
<p>If you’re using uv to manage your virtual environment, simply run the command:</p>
<pre><code class="lang-python">uv add requests
</code></pre>
<p>Then run the code in <code>test.py</code> from within the virtual environment:</p>
<pre><code class="lang-python">uv run python3 test.py
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766419713310/1ebc3435-3826-46fb-93f3-4218c367e280.png" alt="Testing that the running docker container is working by running test.py file" class="image--center mx-auto" width="3710" height="891" loading="lazy"></p>
<p>You should see the desired response on the terminal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766419866358/8f0c2867-64c6-4b16-a5a7-5a0eedf9470f.png" alt="Docker container logs in real time" class="image--center mx-auto" width="3710" height="891" loading="lazy"></p>
<h2 id="heading-how-to-push-your-image-to-amazon-elastic-container-registry-ecr">How to Push Your Image to Amazon Elastic Container Registry (ECR)</h2>
<p>Now that you have a working Docker image to deploy to Lambda, the next step is to push the image to a Docker registry. For this use case, your image has to be pushed to Amazon ECR, a container registry for storing Docker images.</p>
<p>To push your Docker image, you first need to tag the image, which simply means naming the image in a specific way.</p>
<p>Currently, this image tag is <code>lambda-docker:1.0.0</code>. To tag it the AWS way, first create an ECR repository. Let’s use the AWS CLI for this (this requires you to configure the AWS credentials locally by running the <code>aws configure</code> command and providing your credentials).</p>
<h3 id="heading-setup-environment-variables">Setup Environment Variables</h3>
<pre><code class="lang-bash"><span class="hljs-comment"># Set AWS profile</span>
<span class="hljs-built_in">export</span> AWS_PROFILE=&lt;PROFILE_NAME&gt;
</code></pre>
<pre><code class="lang-bash"><span class="hljs-comment"># Set other variables</span>

AWS_REGION=&lt;AWS_REGION&gt;
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REPO_NAME=lambda-docker
TAG=1.0.0
</code></pre>
<p>The above commands set the <code>AWS_PROFILE</code> for the CLI to target the right AWS account for API calls. The other variables specify the region, account ID, and the ECR repository name and tag.</p>
<h3 id="heading-create-ecr-repository-and-authenticate">Create ECR Repository and Authenticate</h3>
<p>Now, create the ECR repository:</p>
<pre><code class="lang-bash">aws ecr create-repository \
  --repository-name <span class="hljs-string">"<span class="hljs-variable">$REPO_NAME</span>"</span> \
  --region <span class="hljs-string">"<span class="hljs-variable">$AWS_REGION</span>"</span>
</code></pre>
<p>Authenticate to Amazon ECR:</p>
<pre><code class="lang-bash">aws ecr get-login-password --region <span class="hljs-string">"<span class="hljs-variable">$AWS_REGION</span>"</span> \
  | docker login \
  --username AWS \
  --password-stdin <span class="hljs-string">"<span class="hljs-variable">$ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com"</span>
</code></pre>
<h3 id="heading-tag-and-push-the-docker-image">Tag and Push the Docker Image</h3>
<p>Now, tag the Docker image:</p>
<pre><code class="lang-bash">docker tag <span class="hljs-variable">$REPO_NAME</span>:<span class="hljs-variable">$TAG</span> \
  <span class="hljs-variable">$ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$REPO_NAME</span>:<span class="hljs-variable">$TAG</span>
</code></pre>
<p>Push the image to the ECR repository you created:</p>
<pre><code class="lang-bash">docker push <span class="hljs-variable">$ACCOUNT_ID</span>.dkr.ecr.<span class="hljs-variable">$AWS_REGION</span>.amazonaws.com/<span class="hljs-variable">$REPO_NAME</span>:<span class="hljs-variable">$TAG</span>
</code></pre>
<p>And that’s it! Your image is now in ECR.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766420761622/5a18e41b-be41-4660-8d6c-59b12aebb4de.jpeg" alt="Image of Amazon ECR showing the repository created earlier" class="image--center mx-auto" width="1920" height="1037" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766420810814/9f65af4b-a509-45e3-be8f-0bed08cfe6b2.png" alt="Image of the docker image pushed to the existing ECR repository" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<h2 id="heading-how-to-deploy-your-docker-image-to-lambda">How to Deploy Your Docker Image to Lambda</h2>
<p>With your image now in ECR, you can create a Lambda function. Navigate to the Lambda console, and click <code>Create a Function</code>.</p>
<h3 id="heading-create-lambda-function">Create Lambda Function</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421062231/19bae74d-a6d5-4e73-8cca-102be40be214.png" alt="AWS Lambda Console" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<p>Select <code>Container Image</code> and go ahead to search for the ECR repository you created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421207358/25ae6eb2-1b1b-43c7-86dc-6dcd512ddc81.jpeg" alt="Select ECR repository to create a Lambda function" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<p>Next, select the image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421335963/ab7d9103-0ea6-4e25-be8c-139344acb5c5.png" alt="Select the existing Docker image from ECR" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<p>Leave other configurations as default and click create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421506518/2f6e631a-a0c7-4f20-966f-2ef87f91bfb7.jpeg" alt="Hit the Create button to create a Lambda function" class="image--center mx-auto" width="1920" height="1033" loading="lazy"></p>
<p>Navigate to the function after creating.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421673261/71c60ac4-35e7-4458-b4a7-1be2440b9e16.jpeg" alt="The newly created Lambda function dashboard/ overview" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<h3 id="heading-test-deployment">Test Deployment</h3>
<p>Now, let’s test the deployment. For this, simply use the existing Lambda <code>Test</code> tab. Provide all the details needed, including the payload for your <code>POST</code> request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421769909/008473e4-bb28-4fdd-8c5b-7e1f3489a3a0.png" alt="Create a new test instance to test the Lambda function" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766421889043/86f6dbe6-be94-4dca-973e-9e7b68064ff3.png" alt="The output of testing Lambda function" class="image--center mx-auto" width="3710" height="1996" loading="lazy"></p>
<p>And that’s it. You’ve successfully deployed a Docker container on AWS by leveraging ECR and Lambda. You can go a step forward by integrating API Gateway and making the function accessible from the internet.</p>
<h2 id="heading-cleanup">Cleanup</h2>
<p>Remember to delete the services you’ve created on your AWS ECR repository and Lambda to avoid extra charges.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Deploying your Docker container on AWS Lambda is an efficient way to get your application running quickly without being bothered by managing servers or platforms.</p>
<p>Thanks for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Full-Stack Serverless CRUD App using AWS and React ]]>
                </title>
                <description>
                    <![CDATA[ Imagine running a production application that automatically scales from zero to thousands of users without ever touching a server configuration. That's the power of serverless architecture, and it's easier to implement than you might think. If you're... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-full-stack-serverless-app/</link>
                <guid isPermaLink="false">68f7b6ca9d4df532a83f12f7</guid>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chisom Uma ]]>
                </dc:creator>
                <pubDate>Tue, 21 Oct 2025 16:37:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761064422167/c0a6b8ed-a500-43f2-820f-42fef5d73275.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine running a production application that automatically scales from zero to thousands of users without ever touching a server configuration. That's the power of serverless architecture, and it's easier to implement than you might think.</p>
<p>If you're a junior cloud engineer ready to move beyond theoretical AWS concepts and build something real, this tutorial walks you through creating a complete serverless coffee shop management system.</p>
<p>You'll learn how to architect, deploy, and secure a production-ready application using AWS's most powerful serverless services.</p>
<p>Without further ado, let's get started!</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-well-be-using">Tools We’ll be Using</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-we-are-building">What We are Building</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-serverless">Why Serverless?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-architectural-overview">Architectural Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-build-a-serverless-full-stack-app">Build a Serverless Full-Stack App</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-create-a-dynamodb-table">Step 1: Create a DynamoDB table</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-an-iam-role-for-the-lambda-function">Step 2: Create an IAM role for the Lambda function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-lambda-layer-and-lambda-functions">Step 3: Create Lambda Layer And Lambda Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-create-an-api-gateway-to-expose-lambda-functions">Step 4: Create an API Gateway To Expose Lambda Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-set-up-react-application-and-upload-build-to-s3-bucket">Step 5: Set up React Application And Upload Build To S3 Bucket</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-set-up-amazon-api-gateway-authorizer">Step 6: Set up Amazon API Gateway Authorizer</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-create-cloudfront-distribution-with-behaviors-for-s3-and-api-gateway">Step 7: Create Cloudfront Distribution With Behaviors For S3 And API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-set-up-react-application-and-upload-build-to-s3-bucket">Step 8: Set up React Application And Upload Build To S3 Bucket</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting-access-denied-error">Troubleshooting Access Denied Error</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-origin-access-control-oac">Step 1: Set up Origin Access Control (OAC)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-update-s3-bucket-policy">Step 2: Update S3 Bucket Policy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-set-default-root-object">Step 3: Set Default Root Object</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Basic knowledge of AWS.</p>
</li>
<li><p>Basic knowledge of AWS serverless services.</p>
</li>
<li><p>Knowledge of React (not required).</p>
</li>
<li><p>Basic knowledge of Postman or other API testing tools.</p>
</li>
</ul>
<h2 id="heading-tools-well-be-using">Tools We’ll be Using</h2>
<ul>
<li><p><a target="_blank" href="https://react.dev/">React.js</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/dynamodb/">DynamoDB</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/api-gateway/">API Gateway</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/pm/cognito/?trk=14a3c368-cab2-4b17-9bd6-1bbec9e89f29&amp;sc_channel=ps&amp;ef_id=Cj0KCQjw9czHBhCyARIsAFZlN8QA0K8iJKGNUsG4QX-JlA1a2EMYyCbYff2A9zo-itZdGqnDcYYJVW4aApzbEALw_wcB:G:s&amp;s_kwcid=AL!4422!3!651541907485!e!!g!!cognito!19835790380!146491699385&amp;gad_campaignid=19835790380&amp;gbraid=0AAAAADjHtp9wvSpEmU_k_hjYPjL8j0lSi&amp;gclid=Cj0KCQjw9czHBhCyARIsAFZlN8QA0K8iJKGNUsG4QX-JlA1a2EMYyCbYff2A9zo-itZdGqnDcYYJVW4aApzbEALw_wcB">Cognito</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/cloudfront/">CloudFront</a></p>
</li>
</ul>
<h2 id="heading-what-we-are-building">What We are Building</h2>
<p>We'll build a complete serverless coffee shop management system using AWS cloud services. Coffee shop owners will securely log in through AWS Cognito authentication and have full control over their inventory, adding new products, updating stock levels, viewing current inventory, and removing discontinued items. To follow along with this tutorial, you can clone the repo <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">here</a>.</p>
<p>This is what our user interface (UI) looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760784475691/8d9ba162-74dd-447d-b627-3e67b8a944ae.png" alt="image of coffee shop dashboard serverless project" class="image--center mx-auto" width="375" height="357" loading="lazy"></p>
<h2 id="heading-why-serverless">Why Serverless?</h2>
<p>AWS serverless services like Lambda, Cognito, and API Gateway automatically scale to zero during quiet periods and instantly ramp up when traffic spikes. While 'serverless' might sound like there are no servers at all, this isn't actually the case. It means that AWS handles all the heavy lifting, provisioning, managing, and scaling of the infrastructure behind the scenes. You only pay for what you use.</p>
<h2 id="heading-architectural-overview">Architectural Overview</h2>
<p>Our architecture uses DynamoDB as the data store, with Lambda functions (enhanced by Lambda layers) handling all API Gateway requests. Cognito secures the API Gateway, while CloudFront CDN delivers everything globally. The React frontend connects directly to the Cognito UserPool and gets hosted on S3 with CloudFront distribution. For production deployments, you can add a custom domain using CloudFlare and AWS Certificate Manager.</p>
<h2 id="heading-build-a-serverless-full-stack-app">Build a Serverless Full-Stack App</h2>
<p>In this section, you’ll build a full-stack serverless architecture.</p>
<h3 id="heading-step-1-create-a-dynamodb-table">Step 1: Create a DynamoDB table</h3>
<p>To create a DynamoDB table, navigate to your AWS console and select the DynamoDB section. You can do this quickly by typing “DynamoDB” into the AWS search bar and clicking on DynamoDB. Next, follow the steps below to complete your table creation:</p>
<ol>
<li><p>Click <strong>Create table</strong>.</p>
</li>
<li><p>Input table name as “CoffeeShop” or anything you want to name it.</p>
</li>
<li><p>Input partition key as “coffeeId” or anything you want to name it.</p>
</li>
<li><p>Click <strong>Create table</strong>.</p>
</li>
</ol>
<p><strong>Step 1.1: Create items</strong></p>
<p>You need to create items for the table. This helps with testing connectivity to your DynamoDB table.</p>
<p>For our use case, we’ll be creating an item in the table called “coffee” and input attributes such as coffeeId, name, price, and availability. To create an item:</p>
<ol>
<li><p>Click <strong>Explore items</strong> on the left navigation pane.</p>
</li>
<li><p>Click <strong>Create items</strong>.</p>
</li>
<li><p>Click the <em>CoffeeShop</em> radio button, then click <strong>Create item</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760785698166/ee1f5e2d-feef-41de-80d8-eb2c4cad4d04.png" alt="image of dynamodb page" class="image--center mx-auto" width="1584" height="731" loading="lazy"></p>
<ol start="4">
<li>Click <strong>Add new attribute</strong>. This allows you to add different data types such as strings and booleans. The JSON structure below shows the attributes created.</li>
</ol>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"coffeeId"</span>: <span class="hljs-string">"c123"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"new cold coffee"</span>,
    <span class="hljs-attr">"price"</span>: <span class="hljs-number">456</span>,
    <span class="hljs-attr">"available"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<h3 id="heading-step-2-create-an-iam-role-for-the-lambda-function">Step 2: Create an IAM role for the Lambda function</h3>
<p>Next, create a Lambda function that interacts with the DynamoDB table using an IAM role attached to the function. We’ll be setting up an IAM role named "CoffeeShopRole" that serves as a shared execution role for all Lambda functions in the coffee shop application.</p>
<p>This role includes the following permissions:</p>
<ul>
<li><p><strong>CloudWatch Logs</strong>: Full logging capabilities (create, write, and manage log streams)</p>
</li>
<li><p><strong>DynamoDB Access</strong>: Complete read, write, update, and delete operations on the "CoffeeShop" table.</p>
</li>
</ul>
<p>To do this:</p>
<ol>
<li><p>Navigate to the AWS IAM console.</p>
</li>
<li><p>Navigate to <strong>Roles</strong>.</p>
</li>
<li><p>Click <strong>Create role</strong>.</p>
</li>
<li><p>Select the Lambda service.</p>
</li>
<li><p>Search for “AWSLambdaBasicExecutionRole.”</p>
</li>
<li><p>Name your role and click <strong>Create role</strong>.</p>
</li>
</ol>
<p>This is what the role looks like:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"VisualEditor0"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"dynamodb:PutItem"</span>,
                <span class="hljs-string">"dynamodb:DeleteItem"</span>,
                <span class="hljs-string">"dynamodb:GetItem"</span>,
                <span class="hljs-string">"dynamodb:Scan"</span>,
                <span class="hljs-string">"dynamodb:UpdateItem"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:dynamodb::&lt;DYNAMODB_TABLE_NAME&gt;"</span>
        },
        {
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"logs:CreateLogGroup"</span>,
                <span class="hljs-string">"logs:CreateLogStream"</span>,
                <span class="hljs-string">"logs:PutLogEvents"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"*"</span>
        }
    ]
}
</code></pre>
<p>This policy allows us to create CloudWatch logs. Next, create an <strong>inline policy</strong> to allow communications to DynamoDB. Select the following actions for the table:</p>
<ul>
<li><p>Get</p>
</li>
<li><p>Put</p>
</li>
<li><p>Update</p>
</li>
<li><p>Scan</p>
</li>
<li><p>Delete</p>
</li>
</ul>
<p>Next, connect your table ARN to the policy by navigating to the created table and copying the ARN into the policy.</p>
<h3 id="heading-step-3-create-lambda-layer-and-lambda-functions">Step 3: Create Lambda Layer And Lambda Functions</h3>
<p>Now, we need to connect our Lambda function to the DynamoDB table. For this, we’ll need the DynamoDB JavaScript SDK. To get started, create two folders: <code>lambda</code> &gt; <code>get</code> in your IDE, preferably VS Code. Navigate into these folders in your terminal and run the <code>npm init</code> command to initialize your project. Update your <code>package.json</code> file with this:</p>
<pre><code class="lang-json">
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"get"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p><strong>Note:</strong> that we’ll be using <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/ECMAScript">ECMAScript</a> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/ECMAScript">throughou</a>t the course of this tutorial.</p>
<p>Next, we have to create a reusable Node.js Lambda layer containing the <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_dynamodb_code_examples.html">DynamoDB JavaScript SDK</a> and shared utility functions. This layer acts like a common library that can be attached to multiple Lambda functions, eliminating the need to bundle the same dependencies repeatedly in each function's deployment package.</p>
<p>To use the SDK, create a new folder in your directory titled <code>index.mjs</code> and paste in the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-comment">// getCoffee function</span>
<span class="hljs-keyword">import</span> { DynamoDBClient, GetItemCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>; <span class="hljs-comment">// ESM import</span>
<span class="hljs-keyword">const</span> config = {
    region: <span class="hljs-string">"us-east-1"</span>,
};
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient(config);
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> coffeeId = <span class="hljs-string">"c123"</span>;
    <span class="hljs-keyword">const</span> input = {
        TableName: <span class="hljs-string">"CoffeShop"</span>,
        Key: {
            coffeeId: {
                S: coffeeId,
            },
        },
    };
    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> GetItemCommand(input);
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(command);
    <span class="hljs-built_in">console</span>.log(response);
    <span class="hljs-keyword">return</span> response;
}
</code></pre>
<p>The code above is the <code>getCoffee</code> function that connects to the DynamoDB table called <code>CoffeShop</code>, looks up the coffee with the ID <code>c123</code>, and displays its details.</p>
<p>Change <code>region</code> to your specific region.</p>
<p>Next, install the Lambda dependencies for the SDK using the command below:</p>
<pre><code class="lang-bash">
npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
</code></pre>
<p>Then, create a zip file for all the current files using the command below:</p>
<pre><code class="lang-bash">zip -r get.zip ./*
</code></pre>
<p>This creates a zip file in your project directory. Now, navigate to the Lambda function page on your AWS console and upload this zip file.</p>
<p>Click <strong>Test</strong> to test your application. If you run into an error, edit the Runtime settings and change the handler name to <code>index.getCoffee</code>. Deploy and run the code again, you should get a successful response from DynamoDB as shown below:</p>
<p>Response:</p>
<pre><code class="lang-bash">
{
  <span class="hljs-string">"<span class="hljs-variable">$metadata</span>"</span>: {
    <span class="hljs-string">"httpStatusCode"</span>: 200,
    <span class="hljs-string">"requestId"</span>: <span class="hljs-string">"R14Q5UMTP3K9P9NAF1OGG0IB57VV4KQNSO5AEMVJF66Q9ASUAAJG"</span>,
    <span class="hljs-string">"attempts"</span>: 1,
    <span class="hljs-string">"totalRetryDelay"</span>: 0
  },
  <span class="hljs-string">"Item"</span>: {
    <span class="hljs-string">"available"</span>: {
      <span class="hljs-string">"BOOL"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-string">"price"</span>: {
      <span class="hljs-string">"N"</span>: <span class="hljs-string">"34"</span>
    },
    <span class="hljs-string">"name"</span>: {
      <span class="hljs-string">"S"</span>: <span class="hljs-string">"My New Coffee"</span>
    },
    <span class="hljs-string">"coffeeId"</span>: {
      <span class="hljs-string">"S"</span>: <span class="hljs-string">"c123"</span>
    }
  }
}
</code></pre>
<p>Now, let’s make the necessary changes to make our function ready for the API gateway to get the API. When someone requests a coffee using the <code>/coffee</code> endpoint, we want the app to returns a list of all coffees. But if the request is made to <code>/coffee/c123</code> or <code>/coffee/id</code>, then the app returns only details about that specific coffee.</p>
<p>To do this, head back to your <code>index.mjs</code> file and paste in the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>;
<span class="hljs-keyword">import</span> { DynamoDBDocumentClient, GetCommand, ScanCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/lib-dynamodb"</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient({});
<span class="hljs-keyword">const</span> docClient = DynamoDBDocumentClient.from(client);
<span class="hljs-keyword">const</span> tableName = process.env.tableName || <span class="hljs-string">"CoffeShop"</span>;
<span class="hljs-keyword">const</span> createResponse = <span class="hljs-function">(<span class="hljs-params">statusCode, body</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> responseBody = <span class="hljs-built_in">JSON</span>.stringify(body);
    <span class="hljs-keyword">return</span> {
        statusCode,
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        body: responseBody,
    };
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { pathParameters } = event;
    <span class="hljs-keyword">const</span> { id } = pathParameters || {};
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">let</span> command;
        <span class="hljs-keyword">if</span> (id) {
            command = <span class="hljs-keyword">new</span> GetCommand({
                TableName: tableName,
                Key: {
                    <span class="hljs-string">"coffeeId"</span>: id,
                },
            });
        }
        <span class="hljs-keyword">else</span> {
            command = <span class="hljs-keyword">new</span> ScanCommand({
                TableName: tableName,
            });
        }
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> docClient.send(command);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">200</span>, response);
    }
    <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching data from DynamoDB:"</span>, err);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">500</span>, { error: err.message });
    }
}
</code></pre>
<p>Run the <code>zip -r get.zip ./*</code> command again and re-upload the zip file in your Lambda function page.</p>
<p>This AWS Lambda function implements a serverless API endpoint for retrieving coffee data from a DynamoDB table, using the <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/GetItemCommand/">AWS SDK v3</a> to create a document client that can either fetch a specific coffee item by ID (when an <code>id</code> parameter is provided in the URL path) or return all items from the table (when no ID is specified, though there's a missing import for <code>ScanCommand</code>).</p>
<p>The function extracts the coffee ID from the incoming event's path parameters, constructs the appropriate DynamoDB command (<code>GetCommand</code> for single items or <code>ScanCommand</code> for all items), executes the database operation, and returns a properly formatted HTTP response with JSON headers and appropriate status codes - either a 200 success response with the coffee data or a 500 error response if something goes wrong during the database operation.</p>
<p>Repeat the steps above for the <code>create</code>, <code>update</code>, and <code>delete</code> functions. You can find these functions in your cloned <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">project repo</a>.</p>
<h3 id="heading-step-4-create-an-api-gateway-to-expose-lambda-functions">Step 4: Create an API Gateway To Expose Lambda Functions</h3>
<p>To create an API that points to the Lambda function:</p>
<ol>
<li><p>Navigate to <strong>API Gateway</strong> &gt; <strong>Routes</strong> and click <strong>Create.</strong></p>
</li>
<li><p>Create the following endpoints.</p>
</li>
</ol>
<pre><code class="lang-bash">
GET /coffee  -&gt; getCoffee lambda <span class="hljs-keyword">function</span>
GET /coffee/{id}  -&gt; getCoffee lambda <span class="hljs-keyword">function</span>
POST /coffee  -&gt; createCoffee lambda <span class="hljs-keyword">function</span>
PUT /coffee/{id}  -&gt; updateCoffee lambda <span class="hljs-keyword">function</span>
DELETE /coffee/{id}  -&gt; deleteCoffee lambda <span class="hljs-keyword">function</span>
</code></pre>
<ol start="3">
<li>Navigate to <strong>Integrations</strong> and create integrations for these endpoints. To do this, go to the <strong>Manage integrations</strong> tab, click <strong>Create,</strong> and select Lambda as the integration target.</li>
</ol>
<p>Now, in your API Gateway portal, click on <code>API: CoffeeShop...(random numbers)</code> and copy the invoke URL for testing, as shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760792772732/1d453e97-ce05-4be2-ae6d-d7eb55f86820.png" alt="image of postman interface during testing" class="image--center mx-auto" width="747" height="644" loading="lazy"></p>
<p>The <code>get</code> request with an <code>id</code> returns a <code>200 OK</code> response with the created items in DynamoDB. You can play around with the rest of the endpoints on Postman :)</p>
<p><strong>Adding Lambda Layer to Solve the Dependency Issue</strong></p>
<p>Before we continue with this tutorial, I’d like to address one problem with the previous steps so far. All functions use the same dependency, but for each function, we had to maintain separate <code>node_modules</code> folders and <code>packages.json</code> files. To fix this issue, we’ll be using <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html">Lamba Layer.</a> Layer contains all the dependencies, while the functions contain only your code.</p>
<p>To get started:</p>
<ol>
<li><p>Create a new folder in your IDE called <code>LambdaWithLayer</code>.</p>
</li>
<li><p>Create two additional folders under the <code>LambdaWithLayer</code> named <code>LambdaFunctionsWithLayer</code> and <code>nodejs</code>.</p>
</li>
</ol>
<p><strong>Note:</strong> You <em>must</em> use the name <code>nodejs</code> for this to work.</p>
<ol start="3">
<li><p>Navigate to the <code>nodejs</code> folder and initialize using the npm init command.</p>
</li>
<li><p>Install dependencies using the command below:</p>
</li>
</ol>
<pre><code class="lang-bash">npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
</code></pre>
<ol start="5">
<li>Create a new file called <code>utils.js</code> under the <code>nodejs</code> folder and paste in the code below:</li>
</ol>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>;
<span class="hljs-keyword">import</span> {
    DynamoDBDocumentClient,
    ScanCommand,
    GetCommand,
    PutCommand,
    UpdateCommand,
    DeleteCommand
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/lib-dynamodb"</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient({});
<span class="hljs-keyword">const</span> docClient = DynamoDBDocumentClient.from(client);
<span class="hljs-keyword">const</span> createResponse = <span class="hljs-function">(<span class="hljs-params">statusCode, body</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> {
        statusCode,
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(body),
    };
};
<span class="hljs-keyword">export</span> {
    docClient,
    createResponse,
    ScanCommand,
    GetCommand,
    PutCommand,
    UpdateCommand,
    DeleteCommand
};
</code></pre>
<p>Here, we imported all the commands for our API operations. Now, we can create Lambda Functions without installing the SDK dependencies for each one. For example, you can create a <code>get</code> folder under the <code>LambdaFunctionsWithLayer</code> folder for the <code>get</code> function, then create an <code>index.mjs</code> file under the <code>get</code> folder. Next, paste the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { docClient, GetCommand, ScanCommand, createResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'/opt/nodejs/utils.mjs'</span>; <span class="hljs-comment">// Import from Layer</span>
<span class="hljs-keyword">const</span> tableName = process.env.tableName || <span class="hljs-string">"CoffeShop"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { pathParameters } = event;
    <span class="hljs-keyword">const</span> { id } = pathParameters || {};
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">let</span> command;
        <span class="hljs-keyword">if</span> (id) {
            command = <span class="hljs-keyword">new</span> GetCommand({
                TableName: tableName,
                Key: {
                    <span class="hljs-string">"coffeeId"</span>: id,
                },
            });
        }
        <span class="hljs-keyword">else</span> {
            command = <span class="hljs-keyword">new</span> ScanCommand({
                TableName: tableName,
            });
        }
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> docClient.send(command);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">200</span>, response);
    }
    <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching data from DynamoDB:"</span>, err);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">500</span>, { error: err.message });
    }
}
</code></pre>
<p>Now we can see that, in the code, we no longer require dependencies for the <code>get</code> function. We just imported from the layer.</p>
<p>Repeat the above steps for other functions.</p>
<p><strong>Note:</strong> You can find the code for other functions in <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">the cloned repo</a>.</p>
<ol start="6">
<li>Create a zip folder for each function. You can do this by creating a file called <code>create_zip.sh</code> under the <code>LambdaFunctionsWithLayer</code> folder. Then paste the script below:</li>
</ol>
<pre><code class="lang-bash">
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for layer"</span>
zip -r layer.zip nodejs
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for GET Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/get
zip -r get.zip index.mjs
mv get.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for POST Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/post
zip -r post.zip index.mjs
mv post.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for UPDATE Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/update
zip -r update.zip index.mjs
mv update.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for DELETE Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/delete
zip -r delete.zip index.mjs
mv delete.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Success!"</span>
</code></pre>
<p>Run the script using the <code>sh create_zip.sh</code> command. This creates zip files (including a <code>layer.zip</code> file) that you can upload to your AWS Lambda function Layer page.</p>
<ol start="7">
<li><p>In your AWS Lambda function page, navigate to <strong>Layers</strong> and upload the <code>layer.zip</code> file**.**</p>
</li>
<li><p>Update the functions by uploading the newly created zip files for each code.</p>
</li>
<li><p>Add the layer to the function by clicking <strong>Layers</strong> in the function view:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760793962650/8e797256-af9e-445d-8025-2fbd29dfe87f.png" alt="image of get coffee lambda layer" class="image--center mx-auto" width="407" height="125" loading="lazy"></p>
<p>Next, click <strong>Add a layer,</strong> then select <strong>Custom layers.</strong> Then choose <strong>“DynamoDBLayer”</strong> and version <strong>“1”.</strong></p>
<ol start="10">
<li><p>Click <strong>Add</strong>.</p>
</li>
<li><p>Repeat for all the other functions.</p>
</li>
</ol>
<h3 id="heading-step-5-set-up-react-application-and-upload-build-to-s3-bucket">Step 5: Set up React Application And Upload Build To S3 Bucket</h3>
<p>To set up our React application, navigate to the <code>frontend</code> folder of the cloned repository on your local machine and run <code>npm install</code> to install the dependencies. Then run <code>npm run dev</code> to start your development environment on your local machine. You should see the preview in your browser at: <a target="_blank" href="http://localhost:5173/"><code>http://localhost:5173/</code></a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794168737/0cace684-7f8b-47db-944a-7a642d991ca0.png" alt="image of coffe list ui" class="image--center mx-auto" width="375" height="357" loading="lazy"></p>
<p>If you inspect the page using <a target="_blank" href="https://developer.chrome.com/docs/devtools">Chrome DevTools</a>, you’ll see that we ran into some <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a> error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794416609/0cd6196e-b7cc-4f61-af5f-77995d6139ec.png" alt="image of chrome dev tool console" class="image--center mx-auto" width="541" height="322" loading="lazy"></p>
<p>Now, let’s fix this problem. To do that:</p>
<ol>
<li><p>Navigate your API Gateway page.</p>
</li>
<li><p>Click on <strong>CORS</strong> on the left navigation panel.</p>
</li>
<li><p>Click <strong>Configure</strong>.</p>
</li>
<li><p>Copy your <a target="_blank" href="http://localhost">localhost</a> URL and paste it into the <strong>Access-Control-Allow-Origin</strong> field.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794511537/26a1917b-16ae-48bc-a786-b36c6bb31490.png" alt="image of cors configuration" class="image--center mx-auto" width="696" height="196" loading="lazy"></p>
<p>Ensure to remove the <code>/</code> at the end of your URL as shown in the image above.</p>
<ol start="5">
<li><p>Click <strong>Add</strong>.</p>
</li>
<li><p>Enter the <strong>Access-Control-Allow-Headers</strong> field with the text content-type and click <strong>Add</strong>.</p>
</li>
<li><p>Include <code>GET</code>, <code>POST</code>, <code>OPTIONS</code>, <code>PUT</code>, and <code>DELETE</code> in <strong>Access-Control-Allow-Methods.</strong></p>
</li>
<li><p>Click <strong>Save</strong>.</p>
</li>
</ol>
<p>Now it returns our coffee, and the CORS error has been resolved.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794692165/7d53573d-f2e0-456d-a1da-0d06265d78a9.png" alt="image of solved cors error" class="image--center mx-auto" width="859" height="578" loading="lazy"></p>
<p>When you add a new coffee, you should see the newly created items in your DynamoDB database.</p>
<h3 id="heading-step-6-set-up-amazon-api-gateway-authorizer">Step 6: Set up Amazon API Gateway Authorizer</h3>
<p>AWS Congnito helps you secure your Amazon API Gateway. Gateway validates the access token with Amazon Cognito to ensure it is valid and has not expired, and grants or denies access based on token validity.</p>
<p>To get started:</p>
<ol>
<li><p>Navigate to <strong>Amazon Cognito &gt; User pools</strong>.</p>
</li>
<li><p>Click <strong>Create user pool</strong>.</p>
</li>
<li><p>Select <strong>Single-page application (SPA)</strong>.</p>
</li>
<li><p>Select email as the preferred sign-in and sign-up method.</p>
</li>
<li><p>Use <code>http://localhost:5174/</code> or your own local URL as the return URL.</p>
</li>
<li><p>Click <strong>Create user directory</strong>.</p>
</li>
</ol>
<p>You’ll be presented with a page containing code that we can copy and paste into our app for integration. But before we do that, let's head back to API Gateway and integrate it with Cognito. To do that:</p>
<ol>
<li><p>Go to the Authorization section in API Gateway.</p>
</li>
<li><p>Navigate to <strong>Manage authorizers</strong>.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Select JWT and name it “Cognito-CoffeeShop”</p>
</li>
<li><p>Copy your issuer URL from Cognito Overview. Your issuer URL is the <em>Token signing key URL</em>. If you click on the URL, you’ll be taken to your browser, where you'll see the keys that’ll be used for verification.</p>
</li>
<li><p>For the Audience, navigate to the Cognito user pool, then to App clients, and select CoffeShopClient. Copy the Client ID.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Go to Routes and add authorizations to each endpoint.</p>
</li>
</ol>
<p>Now, to integrate with our front-end app:</p>
<p>Navigate into the frontend folder and run the command below:</p>
<pre><code class="lang-bash">npm install oidc-client-ts react-oidc-context --save
</code></pre>
<ol start="2">
<li><p>Go to the <strong>App clients</strong> section in Cognito user pools to find the readily available code snippets for integration.</p>
</li>
<li><p>Edit your <code>main.jsx</code> file to include the code below:</p>
</li>
</ol>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>
<span class="hljs-keyword">import</span> { BrowserRouter <span class="hljs-keyword">as</span> Router, Route, Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./index.css'</span>
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.jsx'</span>
<span class="hljs-keyword">import</span> ItemDetails <span class="hljs-keyword">from</span> <span class="hljs-string">"./ItemDetails"</span>;
<span class="hljs-keyword">import</span> { AuthProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-oidc-context"</span>;
<span class="hljs-keyword">const</span> cognitoAuthConfig = {
  authority: <span class="hljs-string">"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_rXq7q3KLm"</span>,
  client_id: <span class="hljs-string">"6fjfrlaup7oph5lhf1q8q6pnp4"</span>,
  redirect_uri: <span class="hljs-string">"http://localhost:5174"</span>,
  response_type: <span class="hljs-string">"code"</span>,
  scope: <span class="hljs-string">"email openid phone"</span>,
};
createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'root'</span>)).render(
  &lt;AuthProvider {...cognitoAuthConfig}&gt;
    &lt;Router&gt;
      &lt;div&gt;
        &lt;Routes&gt;
          &lt;Route path=<span class="hljs-string">"/"</span> element={&lt;App /&gt;} /&gt;
          &lt;Route path=<span class="hljs-string">"/details/:id"</span> element={&lt;ItemDetails /&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  &lt;/AuthProvider&gt;
)
</code></pre>
<p>Here, we imported <code>AuthProvider</code> from <code>react-oidc-context</code>, then wrapped our app with <code>AuthProvider</code>.  Then, move the code in the <code>App.jsx</code> file to a newly created <code>Home.jsx</code> file, and update <code>App.jsx</code> file with the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./App.css"</span>;
<span class="hljs-comment">// App.js</span>
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-oidc-context"</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> auth = useAuth();
  <span class="hljs-keyword">const</span> signOutRedirect = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> clientId = <span class="hljs-string">"6fjfrlaup7oph5lhf1q8q6pnp4"</span>;
    <span class="hljs-keyword">const</span> logoutUri = <span class="hljs-string">"http://localhost:5174/"</span>;
    <span class="hljs-keyword">const</span> cognitoDomain = <span class="hljs-string">"https://us-east-1rxq7q3klm.auth.us-east-1.amazoncognito.com"</span>;
    <span class="hljs-built_in">window</span>.location.href = <span class="hljs-string">`<span class="hljs-subst">${cognitoDomain}</span>/logout?client_id=<span class="hljs-subst">${clientId}</span>&amp;logout_uri=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(logoutUri)}</span>`</span>;
  };
  <span class="hljs-keyword">if</span> (auth.isLoading) {
    <span class="hljs-keyword">return</span> &lt;div&gt;Loading...&lt;/div&gt;;
  }
  <span class="hljs-keyword">if</span> (auth.error) {
    <span class="hljs-keyword">return</span> &lt;div&gt;Encountering error... {auth.error.message}&lt;/div&gt;;
  }
  <span class="hljs-keyword">if</span> (auth.isAuthenticated) {
    <span class="hljs-keyword">return</span> (
      &lt;div&gt;
        &lt;button onClick={<span class="hljs-function">() =&gt;</span> auth.removeUser()}&gt;Sign out&lt;/button&gt;
        &lt;Home /&gt;
      &lt;/div&gt;
    );
  }
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;button onClick={<span class="hljs-function">() =&gt;</span> auth.signinRedirect()}&gt;Sign <span class="hljs-keyword">in</span>&lt;/button&gt;
      &lt;button onClick={<span class="hljs-function">() =&gt;</span> signOutRedirect()}&gt;Sign out&lt;/button&gt;
    &lt;/div&gt;
  );
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>Now, when you run the application again, you should see this login page on your browser:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760795002733/2be7ce35-ecff-41bb-adff-ed13c7a33a32.png" alt="Sign in and Sign out buttons" class="image--center mx-auto" width="272" height="231" loading="lazy"></p>
<p>When you click on Sign in, you’ll get directed to the Sign in page. Click Sign up. You should see the page below to create your account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760795104086/41c44c85-881d-482c-ae1f-d84f1ea76fb5.png" alt="Sign in page with a form" class="image--center mx-auto" width="478" height="493" loading="lazy"></p>
<p>During sign-up, a verification code is sent to your sign-up email. Once you’re logged in, you can then access your coffee dashboard.</p>
<h3 id="heading-step-7-create-cloudfront-distribution-with-behaviors-for-s3-and-api-gateway"><strong>Step 7: Create Cloudfront Distribution With Behaviors For S3 And API Gateway</strong></h3>
<p>To create a distribution.</p>
<ol>
<li><p>Navigate to <strong>CloudFront</strong>.</p>
</li>
<li><p>Click <strong>Create distribution</strong>.</p>
</li>
<li><p>In the Origin page, select the S3 bucket and browse through your created S3 buckets.</p>
</li>
<li><p>Select your coffee shop bucket.</p>
</li>
<li><p>Set origin path to <code>/dist</code>.</p>
</li>
<li><p>Select <em>Origin access control</em> under <strong>Origin access</strong>.</p>
</li>
<li><p>Update your React code and AWS Cognito with the distribution domain name provided in the CloudFront log-in pages tab.</p>
</li>
</ol>
<h3 id="heading-step-8-set-up-react-application-and-upload-build-to-s3-bucket"><strong>Step 8: Set up React Application And Upload Build To S3 Bucket</strong></h3>
<p>In this step, we’ll be building our React application and uploading the static files to an Amazon S3 bucket, which is then served from a CloudFront distribution.</p>
<p>To get started:</p>
<ol>
<li><p>Create an S3 bucket and give it the name “mycoffeeShop123new”. This name should be globally unique across all AWS accounts.</p>
</li>
<li><p>In the frontend folder, run the <code>npm run build</code> command. This creates a <code>dist</code> folder in your directory.</p>
</li>
<li><p>Head back to the S3 bucket and drag-and-drop the <code>dist</code> folder into S3 to upload it.</p>
</li>
<li><p>Click <strong>Upload</strong>.</p>
</li>
</ol>
<p>Now, copy your CloudFront distribution URL and try to access your site in a private browser, for example, Chrome incognito. You should see your site live in the browser.</p>
<h2 id="heading-troubleshooting-access-denied-error"><strong>Troubleshooting Access Denied Error</strong></h2>
<p>You may encounter an access denied error in the browser:</p>
<pre><code class="lang-xml">
<span class="hljs-tag">&lt;<span class="hljs-name">Error</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Code</span>&gt;</span>AccessDenied<span class="hljs-tag">&lt;/<span class="hljs-name">Code</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Message</span>&gt;</span>Access Denied<span class="hljs-tag">&lt;/<span class="hljs-name">Message</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">Error</span>&gt;</span>
</code></pre>
<p>It may be because of a likely S3 + CloudFront configuration error. Here are the steps to resolve this issue:</p>
<h3 id="heading-step-1-set-up-origin-access-control-oac">Step 1: Set up Origin Access Control (OAC)</h3>
<ol>
<li><p>Go to <strong>CloudFront &gt; Your Distribution &gt; Origins tab.</strong></p>
</li>
<li><p>Select your S3 origin and click <strong>Edit.</strong></p>
</li>
<li><p>Under <strong>Origin access</strong>, select <strong>Origin access control settings (recommended)</strong></p>
</li>
<li><p>Click <strong>Create new OAC</strong> (or select an existing one).</p>
</li>
<li><p>Click <strong>Save changes.</strong></p>
</li>
</ol>
<h3 id="heading-step-2-update-s3-bucket-policy">Step 2: Update S3 Bucket Policy</h3>
<p>After saving, CloudFront will show you a <strong>"Copy Policy"</strong> button. Click it, then:</p>
<ol>
<li><p>Go to your S3 bucket &gt; <strong>Permissions</strong> tab.</p>
</li>
<li><p>Scroll to <strong>Bucket policy</strong> and click <strong>Edit.</strong></p>
</li>
<li><p>Paste the copied policy (it should look like this):</p>
</li>
</ol>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowCloudFrontServicePrincipal"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Principal"</span>: {
                <span class="hljs-attr">"Service"</span>: <span class="hljs-string">"cloudfront.amazonaws.com"</span>
            },
            <span class="hljs-attr">"Action"</span>: <span class="hljs-string">"s3:GetObject"</span>,
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::YOUR-BUCKET-NAME/*"</span>,
            <span class="hljs-attr">"Condition"</span>: {
                <span class="hljs-attr">"StringEquals"</span>: {
                    <span class="hljs-attr">"AWS:SourceArn"</span>: <span class="hljs-string">"arn:aws:cloudfront::YOUR-ACCOUNT-ID:distribution/YOUR-DISTRIBUTION-ID"</span>
                }
            }
        }
    ]
}
</code></pre>
<ol start="4">
<li>Click <strong>Save changes.</strong></li>
</ol>
<h3 id="heading-step-3-set-default-root-object"><strong>Step 3: Set Default Root Object</strong></h3>
<ol>
<li><p>Go back to <strong>CloudFront &gt; Your Distribution &gt; General</strong> tab.</p>
</li>
<li><p>Click <strong>Edit.</strong></p>
</li>
<li><p>Set <strong>Default root object</strong> to <code>index.html</code>.</p>
</li>
<li><p>Save changes.</p>
</li>
</ol>
<p>Now, try accessing the site again. It should work.</p>
<p>This brings us to the end of this tutorial. I hope you were able to learn a thing or two about building serverless systems :)</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You've just built a production-ready serverless application from the ground up. You've successfully architected a complete CRUD system that automatically scales, stays secure with Cognito authentication, and costs you only what you actually use.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Securely Deploy APIs to Amazon Lambda – A Practical Guide ]]>
                </title>
                <description>
                    <![CDATA[ Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulner... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-securely-deploy-apis-to-amazon-lambda-a-practical-guide/</link>
                <guid isPermaLink="false">68e8418df4be6f5ede699317</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ aws lambda ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API Gateway ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ secrets management ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Agnes Olorundare ]]>
                </dc:creator>
                <pubDate>Thu, 09 Oct 2025 23:13:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760051580641/75d09121-6167-4e06-94d7-53cf23a6f6a1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulnerable third-party APIs.</p>
<p>Gaps in APIs can occur before requests reach the APIs, within the code housing the APIs, and even along the path of the APIs’ communication with downstream services, dependencies, or other microservices.</p>
<p>Attackers leverage flaws in APIs to gain access to confidential data, harvest or manipulate data, or even make your service unavailable through distributed denial of service attacks.</p>
<p>In this article, you’ll learn to deploy your APIs in Lambda and apply some security measures pre-function, within the function, and post-function.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-an-api">What is an API?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-requirementsprerequisites">Requirements/Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-goal">Project Goal</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-overall-architecture">Project Overall Architecture</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-aws-set-up">AWS Set Up</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clone-project">Clone Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-simple-notification-service">Set Up Simple Notification Service</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-secrets-manager">Set Up Secrets Manager</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-internal-lambda">Set Up Internal Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-external-lambda">Set Up External Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-web-application-firewall">Configure Web Application Firewall</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-cognito-user-pools">Configure Cognito User Pools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-api-gateway">Configure API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-setup-end-to-end">Test Setup End-to-End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clean-up">Clean Up</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-improvements">Improvements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-an-api">What is an API?</h2>
<p>The focus of this article is the security of Application Programming Interfaces (APIs). An API is an interface that connects two programms or applications, allowing them to exchange data and communicate.</p>
<p>An API can be internal to an organization or it can belong to a third-party that allows other users to consume their data through the API.</p>
<h2 id="heading-requirementsprerequisites">Requirements/Prerequisites</h2>
<p>While this tutorial is beginner-friendly, you’ll need the following prerequisites to follow along seamlessly:</p>
<ul>
<li><p>A basic knowledge of the AWS Cloud.</p>
</li>
<li><p>An AWS account with administrator access.</p>
</li>
<li><p>AWS CLI. You can find the installation guide <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">here</a>. Follow the instructions for your operating system.</p>
</li>
<li><p>Python. You can visit Python’s official documentation <a target="_blank" href="https://www.python.org/downloads/">site</a> for a guide on how to download and install Python for your specific operating system.</p>
</li>
<li><p>Pipenv or any Python virtual environment creation tool. You can find the Pipenv installation guide <a target="_blank" href="https://pypi.org/project/pipenv/">here</a><em>.</em></p>
</li>
<li><p>A basic knowledge of Git.</p>
</li>
<li><p>An API client, like Postman or Thunderclient.</p>
</li>
</ul>
<h2 id="heading-project-goal">Project Goal</h2>
<p>By the end of this project, you should be able to deploy APIs in Lambda securely, leveraging AWS cloud-native security services.</p>
<h2 id="heading-project-overall-architecture">Project Overall Architecture</h2>
<p>Below is the architecture of the project workflow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758829544078/b76347ee-bbd3-41f4-88c8-2b3a89ad9087.png" alt="Project Architectural Diagram" class="image--center mx-auto" width="1159" height="464" loading="lazy"></p>
<p>As shown in the architectural diagram, when a user sends a request (a JSON object consisting of the user’s name) to an API hosted in Lambda, the user first gets authenticated by an authentication service called Amazon Cognito.</p>
<p>The request passes through a Web Application Firewall, then an API Gateway. API Gateway will perform a check to see if the user is authorized to access the API using the token that the user sends with the request after authentication. API Gateway then allows the traffic to pass through to the API if the user is authorized.</p>
<p>The user’s request will first get to an External Lambda function, which will then save the user’s name as a message to a Simple Notification Service (SNS) topic. This will then invoke an Internal Lambda to run and log the output in Amazon CloudWatch logs. The SNS topic will be accessed by External Lambda using the SNS’s unique identifier stored in Amazon Secrets Manager.</p>
<h3 id="heading-aws-set-up">AWS Set Up</h3>
<p>You’ll need to set up an AWS environment to get started. This requires creating an account if you don’t already have one.</p>
<p>Following account creation, a root user is automatically created, with all privileges attached to the user. Security best practice is to create another user with administrator privileges and use this user for subsequent tasks.</p>
<p>Then, create an access key for this user, which usually consists of two parts (Access Key ID and Secret Access Key) by navigating to the following:</p>
<p>IAM —→ Users —→ Create Access key</p>
<p>Follow the prompts and choose the <code>Command Line Interface</code> option. Check the <code>Confirmation</code> box, and go on to create the key. Download the CSV file provided, or manually copy the <code>Access Key ID</code> and <code>Secret Access Key</code>. Save them securely.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890397608/a88ec1c6-511c-4a66-aa7a-d0dd3f41f665.png" alt="IAM Dashboard" class="image--center mx-auto" width="951" height="513" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890497481/faab2cb7-b7ba-4e5c-b67e-a00d8fb27a10.png" alt="IAM User Page" class="image--center mx-auto" width="1366" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890928429/1a4b3163-6340-47d2-be3e-0e61c275ba8f.png" alt="Create Access Key Page" class="image--center mx-auto" width="1348" height="551" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890991928/5bb150b6-b014-4398-b839-ee5d6e49c425.png" alt="Access Key Use Option Page" class="image--center mx-auto" width="1366" height="494" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891021874/a2e4eb61-eaca-4732-9377-b499fa7eab5d.png" alt="Set Access Key Tag Page" class="image--center mx-auto" width="1366" height="348" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891049362/372f3a63-8e64-478d-9b06-61f7aa88f73a.png" alt="Download Access Key" class="image--center mx-auto" width="1366" height="517" loading="lazy"></p>
<p>Open up your terminal and run the following commands using the AWS CLI:</p>
<pre><code class="lang-bash">aws configure
</code></pre>
<p>The above command will give some prompts to provide the components of the <code>Access Key</code> created earlier and your default region (the AWS region hosting the service you intend to interact with).</p>
<h3 id="heading-clone-project">Clone Project</h3>
<p>In the next step, you’ll clone the GitHub repository containing the assets and resources used in the project implementation.</p>
<p>Visit the project <a target="_blank" href="https://github.com/Agnes4Him/secure-lambda">URL</a> and clone the repository locally.</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;repository_clone_url&gt;
</code></pre>
<h3 id="heading-set-up-simple-notification-service">Set Up Simple Notification Service</h3>
<p>Amazon Simple Notification Service (SNS) connects system components, enabling asynchronous communication and messaging among them.</p>
<p>Find <code>SNS</code> on the console, click on it, and create a topic that your APIs will send messages to. After successfully creating a topic, navigate to the topic, and in the topic details, you’ll find the topic’s <code>ARN</code>. An ARN is an Amazon Resource Name, and it’s a unique string attached to a resource you’ve created on AWS to help identify the resource. Copy the <code>ARN</code> of the topic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983690093/a2820581-46fb-41d1-aed9-9471a0c2db02.png" alt="SNS Dashboard" class="image--center mx-auto" width="1349" height="517" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758982964553/3eb717c7-8ce3-497b-96fb-c16483cff43e.png" alt="Create SNS Topic" class="image--center mx-auto" width="1366" height="371" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983004729/854335ec-3d53-42e4-8bef-0e8a7d3fb2e6.png" alt="Topic Details" class="image--center mx-auto" width="1348" height="541" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983094356/320ebaf9-9f2d-4241-b747-fd3fd0f0b62b.png" alt="SNS Topic Access Policy" class="image--center mx-auto" width="1348" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983451385/7886c2c4-a52f-4538-8e9d-0d33738f7632.png" alt="Topic Created" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<h3 id="heading-set-up-secrets-manager">Set Up Secrets Manager</h3>
<p>Amazon Secrets Manager is used to store, manage, and retrieve sensitive information such as keys, credentials, tokens, and so on. You’ll store the <code>Topic ARN</code> created earlier. With this approach, you’ll demonstrate how your API can securely access the data and information it needs for its performance.</p>
<p>Go to <code>Secrets Manager</code> on the AWS console and create a secret. Provide the secret’s details, and add a new secret named <code>TOPIC_ARN</code> as the key and the actual SNS Topic ARN as the value.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984342157/38c7855c-2221-4406-9078-496cbb480e47.png" alt="Secrets Manager Console" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984379959/3c4ebdf7-26c8-4b74-a175-0ea2eefd258d.png" alt="Create Secret" class="image--center mx-auto" width="1351" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984459477/cfdd8fce-4a33-45c3-8f12-d6a4a04bf799.png" alt="Choose Other Types of Secret" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984512368/7a76618e-18f6-4b3d-bc03-5941c89909ef.png" alt="Secret Details" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984575543/91ca9360-6320-442a-a121-37f9f35b175b.png" alt="Final Secret Store" class="image--center mx-auto" width="1356" height="314" loading="lazy"></p>
<p>Next, you’ll create some Lambda functions to serve your APIs and consume the output of the APIs. There’re three Lambda functions to set up. Two of the functions will host APIs, each of which can only be accessed by specific users. These will be referred to as <code>ExternalLambda</code>. The third Lambda will consume the output of the External Lambda functions through SNS.</p>
<h3 id="heading-set-up-internal-lambda">Set Up Internal Lambda</h3>
<p>AWS Lambda is a serverless service on AWS that users can leverage to run application functions or code when needed. You’re billed for your Lambda function based on the number of invocations of the function, the duration each invocation lasted, and the amount of memory allocated to the function. Lambda can be provisioned to use any runtime, such as Python or NodeJS. In this demonstration, you’ll focus on the NodeJS runtime.</p>
<p>Now that you know what Lambda is and does, you can create one. Let’s call the first Lambda function InternalLambda. On the AWS console, search for <code>Lambda</code>, and on the <code>Lambda</code> dashboard, click <code>Create a function</code> and provide the details. We’ll be using <code>Node.js</code> – JavaScript at the backend as the runtime of choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140048151/60d5c813-a190-456e-9bad-50b429bdc6f7.png" alt="AWS Lambda" class="image--center mx-auto" width="1351" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140165012/d519f8e9-cb98-4f75-b94e-d4d343e3003c.png" alt="Lambda Details" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p>For the <code>Permissions</code> details, let Lambda create a default <code>IAM Role</code>. This default role is named according to your function, and the permissions attached to the role allow your Lambda function to send logs to CloudWatch, another AWS service used for monitoring and observability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140368576/3f922a46-7b3c-4034-b20b-c2b9ab5dde94.png" alt="Lambda Permissions" class="image--center mx-auto" width="1348" height="528" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146593382/39d50020-d0bf-4cfe-950f-eec8a2ff8989.png" alt="39d50020-d0bf-4cfe-950f-eec8a2ff8989" class="image--center mx-auto" width="1345" height="546" loading="lazy"></p>
<p>As you can see in the last image above, the Lambda function you’ve created needs a <code>trigger</code> and sometimes, a <code>destination</code>. For your <code>InternalLambda</code>, the trigger is the SNS topic we configured earlier. This Lambda will read the messages that’ve been published to it, and then you can access the message from your client or even CloudWatch logs.</p>
<p>To achieve this, click the <code>Add trigger</code> button and provide the details.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140925997/2534535f-5ad3-4e13-99f1-d8bf48c9cec1.png" alt="Add SNS to Lambda" class="image--center mx-auto" width="1358" height="508" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146639798/1f3d75c9-ef5d-4538-9f3a-aaf2e8c0ddbb.png" alt="SNS ARN" class="image--center mx-auto" width="1366" height="533" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146670136/367f7ca9-0b41-41ed-8749-ff70e1770ebb.png" alt="InternalLambda Overview" class="image--center mx-auto" width="1339" height="535" loading="lazy"></p>
<p>Next, you’ll provide the <code>code</code> you want to invoke through Lambda. Find the code in the GitHub repository that you cloned earlier. Paste the code in the Lambda function code space and click on <code>Deploy</code> to deploy the function.</p>
<p><code>secure-lambda/InternalLambda/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request successfully received from SNS'</span>);                            

        <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'Records'</span>][<span class="hljs-number">0</span>][<span class="hljs-string">'Sns'</span>][<span class="hljs-string">'Message'</span>];
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from InternalLambda!`</span>),
        };       
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response: '</span>, response);                                                
        <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (err) {                            
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occurred while processing your request.'</span>),
        };

        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error processing event'</span>, err);
        <span class="hljs-keyword">return</span> response;
    }   
};
</code></pre>
<p>The function defined in the index.js file above is simply taking the <code>event</code> object sent to it from SNS and extracting the <code>Message</code> attribute within it. We’re using <code>console.log</code> here to view outputs from the function and ensure it behaves as expected. Just don’t use this in a production-ready application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759142277747/f4437ff2-4495-485d-b891-d9dda3fc939c.png" alt="InternalLambda Code" class="image--center mx-auto" width="1342" height="550" loading="lazy"></p>
<h3 id="heading-set-up-external-lambda">Set Up External Lambda</h3>
<p>You’ll be creating two external Lambda functions: 1 and 2. These two functions will receive user requests, process them, and publish messages to your SNS topic.</p>
<p>On the Lambda console, create another function and name it <code>ExternalLambda1</code>. Allow Lambda to create a default IAM Role, as previously.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759144966306/ee8a2ed1-5a2e-48df-8556-5dedd7ecdde1.png" alt="Create ExternalLambda1" class="image--center mx-auto" width="1351" height="539" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146732803/82a46fd1-e3e5-4d72-a9fe-b41496ba076b.png" alt="ExternalLambda1 Overview" class="image--center mx-auto" width="1345" height="546" loading="lazy"></p>
<p>Paste the code snippet below in the <code>ExternalLambda1</code> code space:</p>
<p><code>secure-lambda/ExternalLambda1/insex.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);                             <span class="hljs-comment">// added for debugging</span>
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda1! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }  
};
</code></pre>
<p>The code above leverages the AWS SDK to fetch the ARN of the SNS topic created earlier from Secrets Manager. It then publishes a message to the topic.</p>
<p>The SDK already comes installed in the Lambda function. Outside of Lambda, the SDK has to be explicitly installed. The function receives its <code>event</code> from the client via API Gateway, which we’ll configure later.</p>
<p>The SNS topic you created earlier will be the destination for this function. For Lambda to publish a topic to SNS, it needs the necessary permission attached to its IAM Role. AWS can automatically take care of that during your configuration, as shown below.</p>
<p>For the trigger, you’ll use another service known as <code>API Gateway</code>. More on that later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146816330/3c542eff-984e-4d02-85b3-c1da142f94d7.png" alt="ExternalLambda1 Add Destination" class="image--center mx-auto" width="1345" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146778567/9b3650f1-90fd-47ce-99c3-627654f2d41f.png" alt="ExternalLambda1 Destination Permissions" class="image--center mx-auto" width="1348" height="539" loading="lazy"></p>
<p>Follow the same steps to provision another Lambda known as <code>ExternalLambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759145915181/631aa639-493a-4f12-af1e-45f425ca2c16.png" alt="ExternalLambda2" class="image--center mx-auto" width="1351" height="539" loading="lazy"></p>
<p>The outcome of the External Lambda setup is as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146919917/93aaf281-387f-44c3-bf6d-995b076150e9.png" alt="ExternalLambda2 Overview" class="image--center mx-auto" width="1346" height="539" loading="lazy"></p>
<p>Paste the code below into <code>ExternalLambda2</code>. It performs the same function as <code>ExternalLambda1</code>, but their output differ. Each of the two Lambda functions will be receiving traffic to a specific user that’s authorized to access the function.</p>
<p><code>secure-lambda/ExternalLambda2/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);  
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda2! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }              
};
</code></pre>
<p>Before moving on, you need to modify the External Lambda’s IAM Roles. Currently, IAM Roles only have permissions to write to CloudWatch and SNS (automatically added). External Lambda also needs permission to fetch the ARN of the SNS topic that was created earlier.</p>
<p>The point here is to show how to leverage a secrets manager, such as AWS Secrets Manager, to store sensitive information or data, and still access these securely. This approach is more secure than storing the ARN as an environment variable within Lambda.</p>
<p>Navigate to IAM, and click on <code>Policies</code> tab on the left. This brings you to a list of policies. Next, click on <code>Create policy</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320098124/71bde9ad-c6d9-4c0d-8472-d56107708be2.png" alt="IAM Policies" class="image--center mx-auto" width="1348" height="542" loading="lazy"></p>
<p>Search for <code>secrets manager</code> in the Policy editor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320163875/a040af9a-1e92-4aea-8c2e-5c029f60f54e.png" alt="Policy Editor" class="image--center mx-auto" width="1366" height="539" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320923537/c3e8bb54-e78d-498c-9fec-7c7a5225b116.png" alt="Policy Editor2" class="image--center mx-auto" width="1366" height="546" loading="lazy"></p>
<p>Select the permissions Lambda needs to access Secrets Manager. In this case, that would be <code>Read —&gt; GetSecretValue</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321060458/963c1876-dbcc-4d7d-a6fe-9f65281c1a26.png" alt="Policy Editor - Specify Permissions" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p>Select <code>Specific</code> for Resources, and click on <code>Add ARNs</code>. On the next tab, add the details of the Secrets Manager Secret created earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321219657/f50fc354-a238-499c-9009-958bbc624299.png" alt="Policy Editor - Select Access" class="image--center mx-auto" width="1348" height="532" loading="lazy"></p>
<p>The Secret’s ARN will be populated here.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321662642/356fb6bd-adc7-4663-a337-3cfaedb74b2d.png" alt="Policy Editor - Add Secrets Manager ARN" class="image--center mx-auto" width="1351" height="550" loading="lazy"></p>
<p>Next, give the policy a name and create it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321510186/fa5da448-293f-4d95-a3b5-651292a91a7f.png" alt="Policy Editor - Create Policy" class="image--center mx-auto" width="1342" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321721882/27807dda-8ea3-4489-bcab-d03efc201655.png" alt="Newly Added Policy" class="image--center mx-auto" width="1354" height="546" loading="lazy"></p>
<p>Next, navigate to <code>Roles</code>, and search for the IAM Roles assigned to the External Lambda functions. These are named according to the Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321748368/dfc73acb-622c-44f9-9cf8-be51b31e3fe9.png" alt="IAM Roles" class="image--center mx-auto" width="1351" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321856410/f1d4a13c-a568-4c9c-b14f-bb3d24b870f8.png" alt="Lambda IAM Roles" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p>Click <code>Add permissions</code> to add a new permission to the IAM Role selected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322020293/689715ef-7e8c-45cb-9473-010f5aa105fa.png" alt="ExternalLambda1 Role" class="image--center mx-auto" width="1352" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322453532/83996cca-7a05-48fb-8e31-d3fc679df7bc.png" alt="ExternalLambda1 Role - Policy Added" class="image--center mx-auto" width="1349" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322498243/29a8fff5-af9a-4d4c-b3ff-3790b82b6339.png" alt="ExternalLambda2 Role" class="image--center mx-auto" width="1352" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322570298/ab28750d-eb99-40f3-bf48-936b63bba1f0.png" alt="ExternalLambda2 Role - Policy Added" class="image--center mx-auto" width="1350" height="553" loading="lazy"></p>
<h3 id="heading-configure-web-application-firewall">Configure Web Application Firewall</h3>
<p>A firewall is a system placed in front of an application, workload, APIs, and so on to inspect traffic, filter it, and either allow or block the traffic based on some preconfigured rules.</p>
<p>For this project, you’ll use the AWS Web Application Firewall (WAF) service to inspect user requests before routing the traffic to your APIs running in Lambda.</p>
<p>Head over to the AWS console and search for WAF.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759310730367/bbcbdf00-2759-4dd7-9b7b-63ee9c252542.png" alt="AWS Web Application Firewall" class="image--center mx-auto" width="1353" height="546" loading="lazy"></p>
<p>Click on the <code>IP sets</code> tab on the left. This will enable you to create a list of IP addresses that you want to allow (as in this case) or deny.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311298551/4537577d-a574-417e-8352-3f72b3732926.png" alt="IP Sets Page" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311354043/edd29c9c-63e7-4bf6-a503-23ef0af5ac20.png" alt="IP Set Configuration" class="image--center mx-auto" width="1351" height="549" loading="lazy"></p>
<p>The IP addresses should include a CIDR block. For instance, if adding a single IP address, it should be <code>X.X.X.X/32</code>. The same applies to IP address ranges such as <code>X.X.X.X/24</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311560565/0ad16e51-b70b-4a80-98f4-821659fa61b8.png" alt="IP Set Overview" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p>Next, click on the <code>Web ACLs</code> tab, then <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311623780/9742ab87-3303-4046-84df-f9f770ed7c41.png" alt="Web ACL Page" class="image--center mx-auto" width="1366" height="549" loading="lazy"></p>
<p>Choose <code>Regional resources</code> as the Resource type, and enter your region. It’s best to keep all resources you’re creating in this project within the same region. Give your Web ACL a name, then click next.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311736091/b4201885-2dc0-4ed8-aa38-5e25824c363b.png" alt="Web ACL Description" class="image--center mx-auto" width="1351" height="544" loading="lazy"></p>
<p>Add rules to the Web ACL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311892739/5efad662-6c20-4678-b490-54fa33bc3a7b.png" alt="WAF Rule" class="image--center mx-auto" width="1352" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311985197/9e7157c8-bfb4-47a9-a850-67ce8bb302b2.png" alt="Add Rule" class="image--center mx-auto" width="1350" height="551" loading="lazy"></p>
<p>Choose a rule type. In this case, you’ll use <code>IP set</code>, and give the rule a name. Choose the IP set created earlier.</p>
<p>Select <code>Source IP address</code>, and <code>Count</code> as the Action. For this project, you’ll focus on counting the requests sent to your APIs. But as shown in the image below, you can perform other actions, such as allow, block, and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759312925911/ea491527-970c-4b5f-b658-4345ce3d08e4.png" alt="WAF Rule Configuration" class="image--center mx-auto" width="1366" height="548" loading="lazy"></p>
<p>Your final rule configuration will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313133810/8e1be6d3-f6bf-42d9-881d-87216862b3bd.png" alt="WAF Rule Overview" class="image--center mx-auto" width="1350" height="553" loading="lazy"></p>
<p>Scroll down, then click on <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313210947/d625c4f3-a5a3-47c9-961f-ca67f652c992.png" alt="Create Rule" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313261493/3bd16ec5-3376-4607-86cc-fd1716ad68aa.png" alt="Web ACL Dashboard" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<h3 id="heading-configure-cognito-user-pools">Configure Cognito User Pools</h3>
<p>Amazon Cognito is an identity management service used for creating and managing users. You can leverage it to authenticate and authorize users to applications, APIs, or other workloads.</p>
<p>You’ll create <code>User Pools</code> within Cognito and add a user to each pool. You’ll configure how these users can be authenticated and authorized to access the External Lambda functions already created.</p>
<p>Search for <code>Cognito</code> on AWS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315681568/c2e7df4e-0e51-4c03-bf59-41ca895df74d.png" alt="Amazon Cognito" class="image--center mx-auto" width="1356" height="542" loading="lazy"></p>
<p>Click on <code>Get started for free</code>, then <code>Create user pool</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315735324/1c09e934-186f-49db-811f-dd84d7400285.png" alt="Create User Pool" class="image--center mx-auto" width="1366" height="554" loading="lazy"></p>
<p>Select Single-page application (SPA), give the User pool the name <code>MyUserPool1</code>, and select <code>Email</code> as an option for sign-in. This means the main attribute users will provide at signup and sign-in will be their email address. Leave everything else as the default. We’ll keep things as simple as possible.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315828576/73cb66a3-cbde-4443-8dfd-34338091aabc.png" alt="Use Pool Configuration" class="image--center mx-auto" width="1348" height="549" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315901551/9fdc173d-f92b-4080-98d4-513b404a9aeb.png" alt="User Pool Configuration2" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315994247/3fb17ade-90aa-4d47-b2f9-258f6b547a1f.png" alt="User Pool Configuration3" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p>After creating the User pool, you’ll find the page shown below. You can view the login and signup page for the pool you’ve just created by clicking on the <code>View login page</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316208497/4db5f370-deb2-449e-8017-505dc1e13079.png" alt="Cognito App Client Login URL" class="image--center mx-auto" width="1352" height="551" loading="lazy"></p>
<p>You can add <code>App clients</code> to your User Pool. By default, a client named <code>MyUserPool1</code> will be added to the pool. Navigate to your User pool, and click on <code>App clients</code> to see details of this client. Note the <code>Client ID</code>. You’ll also make some edits to the App client by clicking on the <code>Edit</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316443170/58081c40-cdcb-4af9-a60b-79156f6d2d68.png" alt="User Pool App Client Overview" class="image--center mx-auto" width="1348" height="551" loading="lazy"></p>
<p>You’ll edit the <code>Authentication flows</code> field by ticking the <code>Sign in with username and password…</code> and <code>Sign in with server-side administrative credentials…</code> boxes. These changes will enable you to authenticate the user who will be added to this client programmatically, rather than through a UI. With this approach, we can fetch the token assigned to the user by Cognito and use this token to authorize access to Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759317429341/f8db3816-c603-49fe-b661-696bfff98639.png" alt="Edit App Client" class="image--center mx-auto" width="1349" height="549" loading="lazy"></p>
<p>Now, add a user to this pool. The user needs a valid email address. You’ll need the login page URL to create the user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318555729/d9a8ac0c-72d4-4fca-8a94-ff71a5a20caf.png" alt="Cognito Create New User" class="image--center mx-auto" width="1347" height="636" loading="lazy"></p>
<p>You need access to the email used to create the user. Fetch the code sent to the email address and submit to confirm the account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318672240/42d4418d-a4e1-4af9-b8b5-deaf5fb63118.png" alt="Cognito Confirm Email" class="image--center mx-auto" width="1345" height="603" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318710760/4505260f-13c9-4b3b-af99-1cb9e7436147.png" alt="Cognito Successful Sign up" class="image--center mx-auto" width="1366" height="636" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318734000/1b3789d9-917a-4341-84ef-b8e498628557.png" alt="User Pool Users" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Follow the same steps and create another User pool named <code>MyUserPool2</code>. Add a user with a different email to this pool.</p>
<h3 id="heading-configure-api-gateway">Configure API Gateway</h3>
<p>API Gateway is a service used to manage access and route traffic to API backend services such as APIs. It serves as a reverse proxy and provides an extra layer of security for backend services.</p>
<p>You’ll configure API Gateway to direct traffic to your Lambda functions.</p>
<p>Navigate to <code>API Gateway</code> and click on <code>Create an API</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336384538/0e2e4120-ade3-43c3-8a3e-9ec4d0e2b343.png" alt="API Gateway" class="image--center mx-auto" width="1351" height="546" loading="lazy"></p>
<p>Select the <code>REST API</code> option —→ <code>Build</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336533762/56d96fcb-ff27-4f96-b739-fc9658dca50e.png" alt="Select API Type" class="image--center mx-auto" width="1352" height="551" loading="lazy"></p>
<p>Select <code>New API</code>, provide a name, and choose <code>Regional</code> as the API endpoint type. IP address type can be IPv4 or Dualstack. We’ll select IPv4 here. Then create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336585590/ff2b11c2-cb32-4657-913e-6dfc9922531f.png" alt="API Gateway Configuration" class="image--center mx-auto" width="1349" height="549" loading="lazy"></p>
<p>An important part of API Gateway configuration for this project is the Authorizer. API Gateway uses Authorizer to allow traffic from clients to backend services.</p>
<p>You’ll create two Authorizers. Each will be connected to one of the User pools you configured earlier. On the left-hand side of the API Gateway you configured, click on <code>Authorizers</code> —→ <code>Create authorizer</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336744757/bb00a260-3897-4867-96d3-5370e40eae59.png" alt="API Gateway Authorizer" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Provide the name <code>AGAuthorizer1</code>, and select <code>Cognito</code> as the Authorizer type. Add the User pool for MyUserPool1 created earlier. For the Token source, use <code>Authorization</code>. When you send a request from your API client, a token will be added to the request header for authorization. The token’s key will be named <code>Authorization</code>, while the value will be the token itself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337066816/1ef4b11c-3508-49a5-ae7b-561b4c7f4259.png" alt="Authorizer1 Configuration" class="image--center mx-auto" width="1366" height="548" loading="lazy"></p>
<p>Create another Authorization for MyUserPool2 the same way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337472200/aacad54f-8927-4c3e-9a60-f207bbf45577.png" alt="Authorizer2 Configuration" class="image--center mx-auto" width="1366" height="544" loading="lazy"></p>
<p>Both Authorizers will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337540381/56362d9c-7f84-4021-b077-59992abd979b.png" alt="Authorizers Overview" class="image--center mx-auto" width="1366" height="547" loading="lazy"></p>
<p>Next, you’ll create resources and endpoints within the API Gateway that you’ve defined.</p>
<p>A <code>resource</code> in API Gateway is used to group certain endpoints within a specific path. You’ll define two resources within the API Gateway you’ve created. This will create two different paths, &lt;BASE_URL&gt;/&lt;RESOURCE1&gt; and &lt;BASE_URL/RESOURCE2&gt;.</p>
<p>On the API Gateway dashboard, navigate to your Gateway, click on <code>Create resource</code>, define your root path (‘/’ in your case), and provide the resource name (<code>lambda1</code>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420480646/c6e7c1c9-9eee-4dbf-af5b-8c335e14927c.png" alt="API Gateway Lambda1 Resource" class="image--center mx-auto" width="1366" height="528" loading="lazy"></p>
<p>Create another resource named <code>lambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420822042/74d1790e-c752-490c-883f-b42bd00d91eb.png" alt="API Gateway Resources Overview" class="image--center mx-auto" width="1366" height="532" loading="lazy"></p>
<p>Now, click on <code>/lambda1</code>, then <code>Create method</code> to define an endpoint within this resource. You’ll use the <code>POST</code> method to send requests to the backend service via this endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420769955/bd861a6a-884d-4873-aaeb-dc958a0915b1.png" alt="API Gateway Method Configuration" class="image--center mx-auto" width="1349" height="541" loading="lazy"></p>
<p>For the backend service or Integration type, select Lambda function, and provide the ARN of ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421208532/8d84c272-58ad-4ca4-ae56-682151495b76.png" alt="API Gateway Method Configuration2" class="image--center mx-auto" width="1352" height="538" loading="lazy"></p>
<p>For Authorization, select <code>AWS IAM —→ Cognito user pool authorizers —→ AGAuthorizer1</code>. Leave other configurations, then create the endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421234684/1e76c46e-ba07-4178-94b3-e579ed752278.png" alt="API Gateway Method Configuration3" class="image--center mx-auto" width="1352" height="542" loading="lazy"></p>
<p>Repeat the same step to create a <code>POST</code> method for <code>/lambda2</code> resource. The <code>method</code> should be attached to <code>ExternalLambda2</code>, and <code>AGAuthorizer2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421691530/563c85e5-52ff-43fa-8216-41fc269989e0.png" alt="API Gateway Deployment" class="image--center mx-auto" width="1349" height="539" loading="lazy"></p>
<p>The API Gateway you’ve created needs to be deployed to become accessible. Deployment is usually done to a Stage.</p>
<p>Click on <code>Deploy API</code>, select New stage and name the stage development. Then, deploy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421954217/f0ca31c8-78d9-4b1b-a47c-424f6ef32093.png" alt="API Gateway Stage" class="image--center mx-auto" width="1366" height="541" loading="lazy"></p>
<p>After deployment to a stage, an invoke URL will be provided. This will serve as the base URL for the endpoints you’ve defined.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422031311/c7cac4e7-52e9-43dd-a565-46aa062aa364.png" alt="API Gateway Stage Overview" class="image--center mx-auto" width="1345" height="539" loading="lazy"></p>
<p>The stage you’ve created needs some modifications for enhanced security. Firstly, you need to attach the <code>WAF</code> that you created earlier. Secondly, the default rate limit for the API deployed to this stage is 10000. Rate limit restricts excessive resource consumption and protects your API from abuse. For this project, you can reduce the limit to 50.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422132101/b0f67d30-30ec-4d1f-9eee-735dc3b26500.png" alt="Edit API Gateway Stage" class="image--center mx-auto" width="1352" height="542" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759423441047/95dc8f74-c71d-449a-b15a-46caa53c7595.png" alt="API Gateway Stage - Add Rate Limit and WAF" class="image--center mx-auto" width="1352" height="536" loading="lazy"></p>
<p>To test the API Gateway set up, click on the endpoint you want to test, then the <code>Test</code> button. This initial test doesn’t need any authorization, since the test is done directly within the Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435394293/8933349c-3e2a-4795-adb4-bb9eeb990e81.png" alt="API Gateway Endpoint Testing" class="image--center mx-auto" width="1346" height="549" loading="lazy"></p>
<p>Add JSON data as the Request body. The key will be <code>name</code>, and the value will be any string.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435424778/a613816e-8b89-4978-9b3e-6734e48119eb.png" alt="API Gateway Testing2" class="image--center mx-auto" width="1351" height="541" loading="lazy"></p>
<p>The response sent back from ExternalLambda1 shows a status code of 200, and a response body containing exactly the message expected from the Lambda function.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435744694/516bab58-99de-4b41-8926-e9928b8c42e4.png" alt="API Gateway Test Response" class="image--center mx-auto" width="1349" height="540" loading="lazy"></p>
<p>If you head over to CloudWatch Log groups, you should also find the Log groups that were automatically created for the Lambda functions. Click on the Log group for ExternalLambda1 and navigate to the latest Log stream. You should find the logs for the request you’ve just made from API Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435884121/05741f0c-82dd-43f1-855c-157df5c112fc.png" alt="CloudWatch Logs for Testing" class="image--center mx-auto" width="1366" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436073425/d4a0a2c7-8d86-44ce-adb7-4c67c3cdf40b.png" alt="CloudWatch Logs for Testing2" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436150374/5fae1a11-c80d-41df-8ea9-39303287144b.png" alt="CloudWatch Logs - Output from InternalLambda" class="image--center mx-auto" width="1366" height="551" loading="lazy"></p>
<h3 id="heading-test-setup-end-to-end">Test Setup End-to-End</h3>
<p>To test our setup properly, and from the internet, send the same request from your API client with no additional information in the request header. This should return a <code>401</code> error – Unauthorized. This is expected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436452961/342e7659-5a58-45a2-af26-a657b622a83a.png" alt="Request without Token" class="image--center mx-auto" width="1321" height="448" loading="lazy"></p>
<p>API Gateway expects an authorization token from each request it receives before routing traffic to the appropriate backend service. It validates this token through Cognito.</p>
<p>You’ll mimic a user login for each user added to Coginito User pools to get a token for the user. This token will then be sent alongside any request. To achieve this, you’ll use the two Python scripts I’ve provided below:</p>
<p><code>secure-lambda/auth-scripts/user1.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> boto3

client = boto3.client(<span class="hljs-string">"cognito-idp"</span>)

response = client.initiate_auth(
    AuthFlow=<span class="hljs-string">"USER_PASSWORD_AUTH"</span>,  <span class="hljs-comment"># or ADMIN_USER_PASSWORD_AUTH if using admin creds</span>
    AuthParameters={
        <span class="hljs-string">"USERNAME"</span>: <span class="hljs-string">""</span>,             <span class="hljs-comment"># user1 email</span>
        <span class="hljs-string">"PASSWORD"</span>: <span class="hljs-string">""</span>              <span class="hljs-comment"># user1 password</span>
    },
    ClientId=<span class="hljs-string">""</span>                     <span class="hljs-comment"># Cognito App Client ID</span>
)

id_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"IdToken"</span>]
access_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"AccessToken"</span>]
refresh_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"RefreshToken"</span>]

print(<span class="hljs-string">"ID Token:"</span>, id_token)
</code></pre>
<p>Using the Python boto3 library, you’ll initiate an authentication request to Cognito. Provide the email address and password of the user in MyUserPool1. Also, add the Client ID of the App client.</p>
<p>To run the script, create an isolated environment using Pipenv, uv, or a similar library. Install the dependency used in the project as defined in the Pipfile, and run the script with the Pipenv shell.</p>
<pre><code class="lang-bash">pipenv install
pipenv shell
Python secure-lambda/auth-scripts/user1.py
</code></pre>
<p>The Python command will return with a token assigned to the user. Next, you use this token to authorize a user to access ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759437804885/f03ab150-74f9-4ecf-9c7c-0ee07e8662a2.png" alt="Add Token to Request Header" class="image--center mx-auto" width="711" height="446" loading="lazy"></p>
<p>Ensure that the URL for the POST request is in the format: &lt;BASE_URL/lambda1&gt;. You should receive a response from API Gateway indicating success.</p>
<p>Now try accessing ExternalLambda2 using User1 token. You should get an <code>Unauthorized</code> message. Note that user1 will always receive an unauthorized message when it tries accessing ExternalLambda1 without an Authorization token in the header, a wrong token, or when it tries accessing ExternalLambda2, which it is not authorized to access.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438020972/39e2650a-6a99-466a-ad40-6bcf72c491c4.png" alt="User1 Access ExternalLambda2" class="image--center mx-auto" width="1323" height="439" loading="lazy"></p>
<p>Repeat the process with User2 using the token generated for the user in MyUserPool2. First, test access to ExternalLambda2 without a token in the request header.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438058029/37dbffcb-7f58-4ce0-abfa-abf0e6d1d3a4.png" alt="User2 Request without Token" class="image--center mx-auto" width="1366" height="450" loading="lazy"></p>
<p>Then test access with the token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438118097/a408bfe0-6e94-46a1-b98e-c959f31673f8.png" alt="User2 Request with Token" class="image--center mx-auto" width="1311" height="460" loading="lazy"></p>
<p>Next, try accessing ExternalLambda1 using User2.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438144636/06ba905d-ff7b-48d6-8e5b-e35b959221ba.png" alt="User2 Access ExternalLambda1" class="image--center mx-auto" width="1366" height="466" loading="lazy"></p>
<p>You can also view the outcome of some of the requests made by your client on CloudWatch Logs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438188476/2e5aaa35-b52c-4fd3-88f4-00bc704cd809.png" alt="CloudWatch Logs Output" class="image--center mx-auto" width="1366" height="535" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438223720/630bd443-41d2-40b7-b28f-ff937fbf13f9.png" alt="CloudWatch Logs Output2" class="image--center mx-auto" width="1366" height="528" loading="lazy"></p>
<p>Also, since WAF has been configured previously to count requests (although, in a real scenario, you want to achieve much more with WAF, such as allow or block certain traffic), you can view activities captured by WAF by navigating to the service on AWS, then searching for the WAF you configured, and navigating to Traffic overview.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438469300/df718ef7-7eaa-49ad-8780-c43878d2d388.png" alt="WAF - Traffic details" class="image--center mx-auto" width="1366" height="541" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438498828/b1619abf-db45-4946-85e2-53e5a769cdb8.png" alt="WAF - Traffic Details2" class="image--center mx-auto" width="1351" height="541" loading="lazy"></p>
<p>You can find other details, such as the client device types and where requests originated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438552359/477c4f39-04e4-4427-a2d2-74a6066622dd.png" alt="WAF - Traffic Details3" class="image--center mx-auto" width="1348" height="546" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438590513/31acecd0-7f45-4fb9-b352-5dc5fcf49e75.png" alt="WAF - Traffic Details4" class="image--center mx-auto" width="1348" height="547" loading="lazy"></p>
<h3 id="heading-clean-up">Clean Up</h3>
<p>It’s important to clean up the resources created so far after the hands-on exercise. Due to the dependencies among the resources, trying to delete a resource that another resource depends on may lead to an error. So, you should delete them in this order:</p>
<ul>
<li><p>Secrets Manager</p>
</li>
<li><p>Cognito – Users, App Client, then User Pool</p>
</li>
<li><p>API Gateway – Endpoints/ Methods, Resources, API, Stage</p>
</li>
<li><p>Web Application Firewall – IP Set, Web ACL</p>
</li>
<li><p>All Lambda Functions</p>
</li>
<li><p>Lambda IAM Roles and the policies attached to them</p>
</li>
<li><p>CloudWatch Log Group for all the Lambda functions</p>
</li>
<li><p>SNS Topic</p>
</li>
</ul>
<p>Also, you can deactivate or delete the credentials created for your IAM Admin user if not in use.</p>
<h2 id="heading-improvements">Improvements</h2>
<p>Consider the following areas to improve, apply best practices to, and enhance the security posture of your systems further.</p>
<ol>
<li><p>Use of API keys</p>
</li>
<li><p>Third-party API consumption</p>
</li>
<li><p>API inventory management/ documentation</p>
</li>
<li><p>Resource provisioning using Infrastructure as Code</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Security at every layer of an IT system is not negotiable. In this project, we’ve demonstrated how to leverage cloud-native solutions to secure APIs hosted in a serverless service, allowing only authorized users access to the APIs.</p>
<p>I’m Agnes Olorundare, and you can find out more about me on <a target="_blank" href="https://www.linkedin.com/in/agnes-olorundare-446055b8/"><strong>LinkedIn</strong></a><strong><em>.</em></strong></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Machine Learning System on Serverless Architecture ]]>
                </title>
                <description>
                    <![CDATA[ Let’s say you’ve built a fantastic machine learning model that performs beautifully in notebooks. But a model isn’t truly valuable until it’s in production, serving real users and solving real problems. In this article, you’ll learn how to ship a pro... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-machine-learning-system-on-serverless-architecture/</link>
                <guid isPermaLink="false">68addf802314e8b22eae4655</guid>
                
                    <category>
                        <![CDATA[ Deep Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ coding ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kuriko ]]>
                </dc:creator>
                <pubDate>Tue, 26 Aug 2025 16:23:28 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756225357023/04572f1b-b9a7-43e0-aabc-2842faa2703f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Let’s say you’ve built a fantastic machine learning model that performs beautifully in notebooks.</p>
<p>But a model isn’t truly valuable until it’s in production, serving real users and solving real problems.</p>
<p>In this article, you’ll learn how to ship a production-ready ML application built on serverless architecture.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-were-building">What We’re Building</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-ai-pricing-for-retailers">AI Pricing for Retailers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-models">The Models</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tuning-and-training">Tuning and Training</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-prediction">The Prediction</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-performance-validation">Performance Validation</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-the-system-architecture">The System Architecture</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-core-aws-resources-in-the-architecture">Core AWS Resources in the Architecture</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-the-deployment-workflow-in-action">The Deployment Workflow in Action</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-draft-python-scripts">Step 1: Draft Python Scripts</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-configure-featuremodel-stores-in-s3">Step 2: Configure Feature/Model Stores in S3</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-a-flask-application-with-api-endpoints">Step 3: Create a Flask Application with API Endpoints</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-publish-a-docker-image-to-ecr">Step 4: Publish a Docker Image to ECR</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-create-a-lambda-function">Step 5: Create a Lambda Function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-configure-aws-resources">Step 6: Configure AWS Resources</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-building-a-client-application-optional">Building a Client Application (Optional)</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-the-react-application">The React Application</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-final-results">Final Results</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>This project requires some basic experience with:</p>
<ul>
<li><p><strong>Machine Learning / Deep Learning:</strong> The full lifecycle, including data handling, model training, tuning, and validation.</p>
</li>
<li><p><strong>Coding:</strong> Proficiency in Python, with experience using major ML libraries such as PyTorch and Scikit-Learn.</p>
</li>
<li><p><strong>Full-stack deployment:</strong> Experience deploying applications using RESTful APIs.</p>
</li>
</ul>
<h2 id="heading-what-were-building">What We’re Building</h2>
<h3 id="heading-ai-pricing-for-retailers">AI Pricing for Retailers</h3>
<p>This project aims to help a middle-sized retailer compete with large players like Amazon.</p>
<p>Smaller companies often can’t afford significant price discounts, so they can face challenges finding optimal price points as they expand their product lines.</p>
<p>Our goal is to leverage AI models to recommend the best price for a selected product to maximize sales for the retailer, and display it on a client-side user interface (UI):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755873936847/ecf696ef-e161-4453-a6ad-e97d92ac1677.png" alt="What the UI will look like" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can explore the UI from <a target="_blank" href="https://kuriko-iwai.vercel.app/online-commerce-intelligence-hub">here</a>.</p>
<h3 id="heading-the-models">The Models</h3>
<p>I’ll train and tune multiple models so that when the primary model fails, a backup model gets loaded to serve predictions.</p>
<ul>
<li><p><strong>Primary Model</strong>: Multi-layered feedforward network (on the <strong>PyTorch</strong> library)</p>
</li>
<li><p><strong>Backup Models (Backups)</strong>: LightGBM, SVR, and Elastic Net (on the <strong>Scikit-Learn</strong> library)</p>
</li>
</ul>
<p>The backup models are prioritized based on learning capabilities.</p>
<h3 id="heading-tuning-and-training">Tuning and Training</h3>
<p>The primary model was trained on a dataset of around 500,000 samples (<a target="_blank" href="https://archive.ics.uci.edu/dataset/352/online+retail">source)</a> and fine-tuned using <code>Optuna</code>'s Bayesian Optimization, with grid search available for further refinement.</p>
<p>The backups are also trained on the same samples and tuned using the <code>Scikit-Optimize</code> framework.</p>
<h3 id="heading-the-prediction">The Prediction</h3>
<p>All models serve predictions on <strong>logged quantity values.</strong></p>
<p>Logarithmic transformations of the quantity data make the distribution denser, which helps models learn patterns more effectively. This is because logarithms reduce the impact of extreme values, or outliers, and can help normalize skewed data.</p>
<h3 id="heading-performance-validation">Performance Validation</h3>
<p>We’ll evaluate model performance using different metrics for the transformed and original data, with a lower value always indicating better performance.</p>
<ul>
<li><p><strong>Logged values</strong>: Mean Squared Error (MSE)</p>
</li>
<li><p><strong>Actual values</strong>: Root Mean Squared Log Error (RMSLE) and Mean Absolute Error (MAE)</p>
</li>
</ul>
<h2 id="heading-the-system-architecture">The System Architecture</h2>
<p>We’re going to build a complete ecosystem around an <strong>AWS Lambda function</strong> to create a scalable ML system:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:4680/0*ulcNtwJeU5EOfhTg.png" alt="Fig. The system architecture (Created by Kuriko IWAI)" width="600" height="400" loading="lazy"></p>
<p>Fig. The system architecture (Created by <a target="_blank" href="https://kuriko-iwai.vercel.app/">Kuriko IWAI)</a></p>
<p><strong>AWS Lambda</strong> is a <strong>serverless production</strong> where a service provider can run the application without managing servers. Once they upload the code, AWS takes on the responsibility of managing the underlying infrastructure.</p>
<p>In the serverless production, the code is deployed as <strong>a stateless function</strong> that runs only when it’s triggered by an event like HTTP requests or scheduled tasks.</p>
<p>This event-driven nature makes serverless production extremely efficient in resource allocation because:</p>
<ul>
<li><p><strong>There’s no server management</strong>: The cloud provider takes care of operational tasks.</p>
</li>
<li><p><strong>You have automatic scaling</strong>: Serverless applications automatically scale up or down based on demand.</p>
</li>
<li><p><strong>You have pay-per-use billing</strong>: Charged for the exact amount of compute resources the application consumes.</p>
</li>
</ul>
<p>Note that other cloud ecosystems like Google Cloud Platform (GCP) and Microsoft Azure offer comprehensive alternatives to AWS. Which one you choose depends on your budget, project type, and familiarity with each ecosystem.</p>
<h3 id="heading-core-aws-resources-in-the-architecture">Core AWS Resources in the Architecture</h3>
<p>The system architecture focuses on the following points:</p>
<ul>
<li><p>The application is fully containerized on Docker for universal accessibility.</p>
</li>
<li><p>The container image is stored in AWS Elastic Container Registry (ECR).</p>
</li>
<li><p>The API Gateway’s REST API endpoints trigger an event to invoke the Lambda function.</p>
</li>
<li><p>The Lambda function loads the container image from ECR and perform inference.</p>
</li>
<li><p>Trained models, processors, and input features are stored in AWS S3 buckets.</p>
</li>
<li><p>A Redis client serves cached analytical data and past predictions stored in the ElastiCache.</p>
</li>
</ul>
<p>And to build the system, we’ll use the following AWS resources:</p>
<ul>
<li><p><strong>Lamda</strong>: Serves a function to perform inference.</p>
</li>
<li><p><strong>API Gateway:</strong> Routes API calls to the Lambda function.</p>
</li>
<li><p><strong>S3 Storage</strong>: Serves feature store and model store.</p>
</li>
<li><p><strong>ElastiCache:</strong> Store cached predictions and analytical data.</p>
</li>
<li><p><strong>ECR</strong>: Stores Docker container images to allow Lambda to pull the image.</p>
</li>
</ul>
<p>Each resource requires configuration. I’ll explore those details in the next section.</p>
<h2 id="heading-the-deployment-workflow-in-action"><strong>The Deployment Workflow in Action</strong></h2>
<p>The deployment workflow involves the following steps:</p>
<ol>
<li><p>Draft data preparation, model training, and serialization scripts</p>
</li>
<li><p>Configure designated feature store and model store in S3</p>
</li>
<li><p>Create a Flask application with API endpoints</p>
</li>
<li><p>Publish a Docker image to ECR</p>
</li>
<li><p>Create a Lambda function</p>
</li>
<li><p>Configure related AWS resources</p>
</li>
</ol>
<p>We’ll now walk through each of these steps to help you fully understand the process.</p>
<p>For your reference, here is the repository structure:</p>
<pre><code class="lang-markdown">.
.venv/                  [.gitignore]    # stores uv venv
│
└── data/               [.gitignore]
│     └──raw/                           # stores raw data
│     └──preprocessed/                  # stores processed data after imputation and engineering
│
└── models/             [.gitignore]    # stores serialized model after training and tuning
│     └──dfn/                           # deep feedforward network
│     └──gbm/                           # light gbm
│     └──en/                            # elastic net
│     └──production/                    # models to be stored in S3 for production use
|
└── notebooks/                          # stores experimentation notebooks
│
└── src/                                # core functions
│     └──<span class="hljs-emphasis">_utils/                        # utility functions
│     └──data_</span>handling/                 # functions to engineer features
│     └──model/                         # functions to train, tune, validate models
│     │     └── sklearn<span class="hljs-emphasis">_model
│     │     └── torch_</span>model
│     │     └── ...
│     └──main.py                        # main script to run the inference locally
│
└──app.py                               # Flask application (API endpoints)
└──pyproject.toml                       # project configuration
└──.env                [.gitignore]     # environment variables
└──uv.lock                              # dependency locking
└──Dockerfile                           # for Docker container image
└──.dockerignore
└──requirements.txt
└──.python-version                      # python version locking (3.12)
</code></pre>
<h3 id="heading-step-1-draft-python-scripts">Step 1: Draft Python Scripts</h3>
<p>The first step is to draft Python scripts for data preparation, model training and tuning.</p>
<p>We’ll run these scripts in a <strong>batch process</strong> because these are resource-intensive and stateful tasks that aren’t suitable for serverless functions optimized for short-lived, stateless, and event-driven tasks.</p>
<p>Serverless functions also can experience <a target="_blank" href="https://www.freecodecamp.org/news/cold-start-problem-in-recommender-systems/"><strong>cold starts</strong></a>. With heavy tasks in the function, the API gateway would timeout before serving predictions.</p>
<p><code>src/main.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> torch
<span class="hljs-keyword">import</span> warnings
<span class="hljs-keyword">import</span> pickle
<span class="hljs-keyword">import</span> joblib
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> lightgbm <span class="hljs-keyword">as</span> lgb
<span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> ElasticNet
<span class="hljs-keyword">from</span> sklearn.svm <span class="hljs-keyword">import</span> SVR
<span class="hljs-keyword">from</span> skopt.space <span class="hljs-keyword">import</span> Real, Integer, Categorical
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-keyword">import</span> src.data_handling <span class="hljs-keyword">as</span> data_handling
<span class="hljs-keyword">import</span> src.model.torch_model <span class="hljs-keyword">as</span> t
<span class="hljs-keyword">import</span> src.model.sklearn_model <span class="hljs-keyword">as</span> sk


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>: 
    load_dotenv(override=<span class="hljs-literal">True</span>)
    os.makedirs(PRODUCTION_MODEL_FOLDER_PATH, exist_ok=<span class="hljs-literal">True</span>)

    <span class="hljs-comment"># create train, validation, test datasets</span>
    X_train, X_val, X_test, y_train, y_val, y_test, preprocessor = data_handling.main_script()

    <span class="hljs-comment"># store the trained preprocessor in local storage</span>
    joblib.dump(preprocessor, PREPROCESSOR_PATH)

    <span class="hljs-comment"># model tuning and training</span>
    best_dfn_full_trained, checkpoint = t.main_script(X_train, X_val, y_train, y_val)

    <span class="hljs-comment"># serialize the trained model</span>
    torch.save(checkpoint, DFN_FILE_PATH)

    <span class="hljs-comment"># svr</span>
    best_svr_trained, best_hparams_svr = sk.main_script(
        X_train, X_val, y_train, y_val, **sklearn_models[<span class="hljs-number">1</span>]
    )
    <span class="hljs-keyword">if</span> best_svr_trained <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">with</span> open(SVR_FILE_PATH, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> f:
            pickle.dump({ <span class="hljs-string">'best_model'</span>: best_svr_trained, <span class="hljs-string">'best_hparams'</span>: best_hparams_svr }, f)

    <span class="hljs-comment"># elastic net</span>
    best_en_trained, best_hparams_en = sk.main_script(
        X_train, X_val, y_train, y_val, **sklearn_models[<span class="hljs-number">0</span>]
    )
    <span class="hljs-keyword">if</span> best_en_trained <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">with</span> open(EN_FILE_PATH, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> f:
            pickle.dump({ <span class="hljs-string">'best_model'</span>: best_en_trained, <span class="hljs-string">'best_hparams'</span>: best_hparams_en }, f)

    <span class="hljs-comment"># light gbm</span>
    best_gbm_trained, best_hparams_gbm = sk.main_script(
        X_train, X_val, y_train, y_val, **sklearn_models[<span class="hljs-number">2</span>]
    )

    <span class="hljs-keyword">if</span> best_gbm_trained <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">with</span> open(GBM_FILE_PATH, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> f:
            pickle.dump({<span class="hljs-string">'best_model'</span>: best_gbm_trained, <span class="hljs-string">'best_hparams'</span>: best_hparams_gbm }, f)
</code></pre>
<p>Run the script to train and serialize the models using the <code>uv</code> package management:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$uv</span> venv
<span class="hljs-variable">$source</span> .venv/bin/activate
<span class="hljs-variable">$uv</span> run src/main.py
</code></pre>
<p>The <code>main.py</code> script includes several key components.</p>
<h4 id="heading-scripts-for-data-handling">Scripts for Data Handling</h4>
<p>These scripts involve loading original data, structure missing values, and engineer features necessary for the future prediction.</p>
<p><code>src/data_handling/main.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> joblib
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> train_test_split

<span class="hljs-keyword">import</span> src.data_handling.scripts <span class="hljs-keyword">as</span> scripts
<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> main_logger


<span class="hljs-comment"># load and save the original data frame in parquet</span>
df = scripts.load_original_dataframe()
df.to_parquet(ORIGINAL_DF_PATH, index=<span class="hljs-literal">False</span>)

<span class="hljs-comment"># imputation</span>
df = scripts.structure_missing_values(df=df)

<span class="hljs-comment"># feature engineering</span>
df = scripts.handle_feature_engineering(df=df)

<span class="hljs-comment"># save processed df in csv and parquet</span>
scripts.save_df_to_csv(df=df)
df.to_parquet(PROCESSED_DF_PATH, index=<span class="hljs-literal">False</span>)


<span class="hljs-comment"># for preprocessing, classify numerical and categorical columns</span>
num_cols, cat_cols = scripts.categorize_num_cat_cols(df=df, target_col=target_col)
<span class="hljs-keyword">if</span> cat_cols:
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> cat_cols: df[col] = df[col].astype(<span class="hljs-string">'string'</span>)

<span class="hljs-comment"># creates training, validation, and test datasets (test dataset is for inference only)</span>
y = df[target_col]
X = df.copy().drop(target_col, axis=<span class="hljs-string">'columns'</span>)
test_size, random_state = <span class="hljs-number">50000</span>, <span class="hljs-number">42</span>
X_tv, X_test, y_tv, y_test = train_test_split(
    X, y, test_size=test_size, random_state=random_state
)
X_train, X_val, y_train, y_val = train_test_split(
    X_tv, y_tv, test_size=test_size, random_state=random_state
)

<span class="hljs-comment"># transform the input datasets</span>
X_train, X_val, X_test, preprocessor = scripts.transform_input(
    X_train, X_val, X_test, num_cols=num_cols, cat_cols=cat_cols
)

<span class="hljs-comment"># retrain and serialize the preprocessor</span>
<span class="hljs-keyword">if</span> preprocessor <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>: preprocessor.fit(X)
joblib.dump(preprocessor, PREPROCESSOR_PATH)
</code></pre>
<h4 id="heading-scripts-for-model-training-and-tuning-pytorch-model">Scripts for Model Training and Tuning (PyTorch Model)</h4>
<p>The scripts involve initiating the model, searching optimal neural architecture and hyperparameters, and serializing the fully-trained model so that the system can load the trained model when performing inference.</p>
<p>Because the primary model is built on PyTorch and the backups use Scikit-Learn, we’re drafting the scripts separately.</p>
<h4 id="heading-1-pytorch-models">1. PyTorch Models</h4>
<p><strong>The training script</strong> contains training the model with the validation over a subset of training data.</p>
<p>It contains the early stopping logic when the loss history is not improved for a given consecutive epochs (that is, 10 epochs).</p>
<p><code>src/model/torch_model/scripts/training.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch
<span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn
<span class="hljs-keyword">import</span> optuna <span class="hljs-comment"># type: ignore</span>
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> train_test_split

<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> main_logger

<span class="hljs-comment"># device</span>
device_type = device_type <span class="hljs-keyword">if</span> device_type <span class="hljs-keyword">else</span> <span class="hljs-string">'cuda'</span> <span class="hljs-keyword">if</span> torch.cuda.is_available() <span class="hljs-keyword">else</span> <span class="hljs-string">'mps'</span> <span class="hljs-keyword">if</span> torch.backends.mps.is_available() <span class="hljs-keyword">else</span> <span class="hljs-string">'cpu'</span>
device = torch.device(device_type)

<span class="hljs-comment"># gradient scaler for stability (only applicable for cuba)</span>
scaler = torch.GradScaler(device=device_type) <span class="hljs-keyword">if</span> device_type == <span class="hljs-string">'cuba'</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>

<span class="hljs-comment"># start training</span>
best_val_loss = float(<span class="hljs-string">'inf'</span>)
epochs_no_improve = <span class="hljs-number">0</span>
<span class="hljs-keyword">for</span> epoch <span class="hljs-keyword">in</span> range(num_epochs):
    model.train()
    <span class="hljs-keyword">for</span> batch_X, batch_y <span class="hljs-keyword">in</span> train_data_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)
        optimizer.zero_grad()

        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># pytorch's AMP system automatically handles the casting of tensors to Float16 or Float32</span>
            <span class="hljs-keyword">with</span> torch.autocast(device_type=device_type):
                outputs = model(batch_X)
                loss = criterion(outputs, batch_y)

                <span class="hljs-comment"># break the training loop when models return nan or inf</span>
                <span class="hljs-keyword">if</span> torch.any(torch.isnan(outputs)) <span class="hljs-keyword">or</span> torch.any(torch.isinf(outputs)):
                    main_logger.error(
                        <span class="hljs-string">'pytorch model returns nan or inf. break the training loop.'</span>
                    )
                    <span class="hljs-keyword">break</span>

            <span class="hljs-comment"># create scaled gradients of losses</span>
            <span class="hljs-keyword">if</span> scaler <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
                scaler.scale(loss).backward()
                scaler.unscale_(optimizer)  <span class="hljs-comment"># cliping grad</span>
                nn.utils.clip_grad_norm_(model.parameters(), max_norm=<span class="hljs-number">1.0</span>)
                scaler.step(optimizer)  <span class="hljs-comment"># unscales the gradients</span>
                scaler.update()  <span class="hljs-comment"># updates the scale</span>

            <span class="hljs-keyword">else</span>:
                loss.backward()
                nn.utils.clip_grad_norm_(model.parameters(), max_norm=<span class="hljs-number">1.0</span>) <span class="hljs-comment"># cliping grad</span>
                optimizer.step()

        <span class="hljs-keyword">except</span>:
            outputs = model(batch_X)
            loss = criterion(outputs, batch_y)
            loss.backward()
            optimizer.step()


    <span class="hljs-comment"># run validation on a subset of the training dataset</span>
    model.eval()
    val_loss = <span class="hljs-number">0.0</span>

    <span class="hljs-comment"># switch the torch mode</span>
    <span class="hljs-keyword">with</span> torch.inference_mode():
        <span class="hljs-keyword">for</span> batch_X_val, batch_y_val <span class="hljs-keyword">in</span> val_data_loader:
            batch_X_val, batch_y_val = batch_X_val.to(device), batch_y_val.to(device)
            outputs_val = model(batch_X_val)
            val_loss += criterion(outputs_val, batch_y_val).item()

    val_loss /= len(val_data_loader)

    <span class="hljs-comment"># check if early stop</span>
    <span class="hljs-keyword">if</span> val_loss &lt; best_val_loss - min_delta:
        best_val_loss = val_loss
        epochs_no_improve = <span class="hljs-number">0</span>
    <span class="hljs-keyword">else</span>:
        epochs_no_improve += <span class="hljs-number">1</span>
        <span class="hljs-keyword">if</span> epochs_no_improve &gt;= patience:
            main_logger.info(<span class="hljs-string">f'early stopping at epoch <span class="hljs-subst">{epoch + <span class="hljs-number">1</span>}</span>'</span>)
            <span class="hljs-keyword">break</span>
</code></pre>
<p><strong>The tuning script</strong> uses the <code>study</code> component from the <code>Optuna</code> library to run the Bayesian Optimization.</p>
<p>The <code>study</code> component choose a neural architecture and hyperparameter set to test from the global search space.</p>
<p>Then, it builds, trains, and validates the model to find the optimal neural architecture that can minimize the loss (MSE, for instance).</p>
<p><code>src/model/torch_model/scripts/tuning.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> itertools
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> optuna
<span class="hljs-keyword">import</span> torch
<span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn
<span class="hljs-keyword">import</span> torch.optim <span class="hljs-keyword">as</span> optim
<span class="hljs-keyword">from</span> torch.utils.data <span class="hljs-keyword">import</span> DataLoader, TensorDataset
<span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> train_test_split

<span class="hljs-keyword">from</span> src.model.torch_model.scripts.pretrained_base <span class="hljs-keyword">import</span> DFN
<span class="hljs-keyword">from</span> src.model.torch_model.scripts.training <span class="hljs-keyword">import</span> train_model
<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> main_logger

<span class="hljs-comment"># device</span>
device_type = <span class="hljs-string">"cuda"</span> <span class="hljs-keyword">if</span> torch.cuda.is_available() <span class="hljs-keyword">else</span> <span class="hljs-string">"mps"</span> <span class="hljs-keyword">if</span> torch.backends.mps.is_available() <span class="hljs-keyword">else</span> <span class="hljs-string">"cpu"</span>
device = torch.device(device_type)

<span class="hljs-comment"># loss function</span>
criterion = nn.MSELoss()

<span class="hljs-comment"># define objective function for optuna</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">objective</span>(<span class="hljs-params">trial</span>):</span>
    <span class="hljs-comment"># model</span>
    num_layers = trial.suggest_int(<span class="hljs-string">'num_layers'</span>, <span class="hljs-number">1</span>, <span class="hljs-number">20</span>)
    batch_norm = trial.suggest_categorical(<span class="hljs-string">'batch_norm'</span>, [<span class="hljs-literal">True</span>, <span class="hljs-literal">False</span>])
    dropout_rates = []
    hidden_units_per_layer = []
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(num_layers):
        dropout_rates.append(trial.suggest_float(<span class="hljs-string">f'dropout_rate_layer_<span class="hljs-subst">{i}</span>'</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.6</span>))
        hidden_units_per_layer.append(trial.suggest_int(<span class="hljs-string">f'n_units_layer_<span class="hljs-subst">{i}</span>'</span>, <span class="hljs-number">8</span>, <span class="hljs-number">256</span>)) <span class="hljs-comment"># hidden units per layer</span>

    model = DFN(
        input_dim=X_train.shape[<span class="hljs-number">1</span>],
        num_layers=num_layers,
        dropout_rates=dropout_rates,
        batch_norm=batch_norm,
        hidden_units_per_layer=hidden_units_per_layer
    ).to(device)

    <span class="hljs-comment"># optimizer</span>
    learning_rate = trial.suggest_float(<span class="hljs-string">'learning_rate'</span>, <span class="hljs-number">1e-10</span>, <span class="hljs-number">1e-1</span>, log=<span class="hljs-literal">True</span>)
    optimizer_name = trial.suggest_categorical(<span class="hljs-string">'optimizer'</span>, [<span class="hljs-string">'adam'</span>, <span class="hljs-string">'rmsprop'</span>, <span class="hljs-string">'sgd'</span>, <span class="hljs-string">'adamw'</span>, <span class="hljs-string">'adamax'</span>, <span class="hljs-string">'adadelta'</span>, <span class="hljs-string">'radam'</span>])
    optimizer = _handle_optimizer(optimizer_name=optimizer_name, model=model, lr=learning_rate)

    <span class="hljs-comment"># data loaders</span>
    batch_size = trial.suggest_categorical(<span class="hljs-string">'batch_size'</span>, [<span class="hljs-number">32</span>, <span class="hljs-number">64</span>, <span class="hljs-number">128</span>, <span class="hljs-number">256</span>])
    test_size = <span class="hljs-number">10000</span> <span class="hljs-keyword">if</span> len(X_train) &gt; <span class="hljs-number">15000</span> <span class="hljs-keyword">else</span> int(len(X_train) * <span class="hljs-number">0.2</span>)
    X_train_search, X_val_search, y_train_search, y_val_search = train_test_split(X_train, y_train, test_size=test_size, random_state=<span class="hljs-number">42</span>)
    train_data_loader = create_torch_data_loader(X=X_train_search, y=y_train_search, batch_size=batch_size)
    val_data_loader = create_torch_data_loader(X=X_val_search, y=y_val_search, batch_size=batch_size)

    <span class="hljs-comment"># training</span>
    num_epochs = <span class="hljs-number">3000</span> <span class="hljs-comment"># ensure enough epochs (early stopping would stop the loop when overfitting)</span>
    _, best_val_loss = train_model(
        train_data_loader=train_data_loader,
        val_data_loader=val_data_loader,
        model=model,
        optimizer=optimizer,
        criterion = criterion,
        num_epochs=num_epochs,
        trial=trial,
    )
    <span class="hljs-keyword">return</span> best_val_loss


<span class="hljs-comment"># start to optimize hyperparameters and architecture</span>
study = optuna.create_study(direction=<span class="hljs-string">'minimize'</span>, sampler=optuna.samplers.TPESampler())
study.optimize(objective, n_trials=<span class="hljs-number">50</span>, timeout=<span class="hljs-number">600</span>)

<span class="hljs-comment"># best </span>
best_trial = study.best_trial
best_hparams = best_trial.params

<span class="hljs-comment"># construct the model based on the tuning results</span>
best_lr = best_hparams[<span class="hljs-string">'learning_rate'</span>]
best_batch_size = best_hparams[<span class="hljs-string">'batch_size'</span>]
input_dim = X_train.shape[<span class="hljs-number">1</span>]
best_model = DFN(
    input_dim=input_dim,
    num_layers=best_hparams[<span class="hljs-string">'num_layers'</span>],
    hidden_units_per_layer=[v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> best_hparams.items() <span class="hljs-keyword">if</span> <span class="hljs-string">'n_units_layer_'</span> <span class="hljs-keyword">in</span> k],
    batch_norm=best_hparams[<span class="hljs-string">'batch_norm'</span>],
    dropout_rates=[v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> best_hparams.items() <span class="hljs-keyword">if</span> <span class="hljs-string">'dropout_rate_layer_'</span> <span class="hljs-keyword">in</span> k],
).to(device)

<span class="hljs-comment"># construct an optimizer based on the tuning results</span>
best_optimizer_name = best_hparams[<span class="hljs-string">'optimizer'</span>]
best_optimizer = _handle_optimizer(
    optimizer_name=best_optimizer_name, model=best_model, lr=best_lr
)

<span class="hljs-comment"># create torch data loaders</span>
train_data_loader = create_torch_data_loader(
    X=X_train, y=y_train, batch_size=best_batch_size
)
val_data_loader = create_torch_data_loader(
    X=X_val, y=y_val, batch_size=best_batch_size
)

<span class="hljs-comment"># retrain the best model with full training dataset applying the optimal batch size and optimizer</span>
best_model, _ = train_model(
    train_data_loader=train_data_loader,
    val_data_loader=val_data_loader,
    model=best_model,
    optimizer=best_optimizer,
    criterion = criterion,
    num_epochs=<span class="hljs-number">1000</span>
)

<span class="hljs-comment"># create a checkpoint for serialization (reconstruct the model using the checkpoint)</span>
checkpoint = {
    <span class="hljs-string">'state_dict'</span>: best_model.state_dict(),
    <span class="hljs-string">'hparams'</span>: best_hparams,
    <span class="hljs-string">'input_dim'</span>: X_train.shape[<span class="hljs-number">1</span>],
    <span class="hljs-string">'optimizer'</span>: best_optimizer,
    <span class="hljs-string">'batch_size'</span>: best_batch_size
}

<span class="hljs-comment"># serialize the model w/ checkpoint</span>
torch.save(checkpoint, FILE_PATH)
</code></pre>
<h4 id="heading-2-scikit-learn-models-backups">2. Scikit-Learn Models (Backups)</h4>
<p>For Scikit-Learn models, we’ll run <strong>k-fold cross validation</strong> during training to prevent overfitting.</p>
<p>K-fold cross-validation is a technique for evaluating a machine learning model's performance by training and testing it on different subsets of training data.</p>
<p>We define the <code>run_kfold_validation</code> function where the model is trained and validated using <strong>5-fold cross-validation</strong>.</p>
<p><code>src/model/sklearn_model/scripts/tuning.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.model_selection <span class="hljs-keyword">import</span> KFold
<span class="hljs-keyword">from</span> sklearn.metrics <span class="hljs-keyword">import</span> mean_squared_error

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_kfold_validation</span>(<span class="hljs-params">
        X_train,
        y_train,
        base_model,
        hparams: dict,
        n_splits: int = <span class="hljs-number">5</span>, <span class="hljs-comment"># the number of folds </span>
        early_stopping_rounds: int = <span class="hljs-number">10</span>,
        max_iters: int = <span class="hljs-number">200</span>
    </span>) -&gt; float:</span>

    mses = <span class="hljs-number">0.0</span>

    <span class="hljs-comment"># create k-fold component</span>
    kf = KFold(n_splits=n_splits, shuffle=<span class="hljs-literal">True</span>, random_state=<span class="hljs-number">42</span>)

    <span class="hljs-keyword">for</span> fold, (train_index, val_index) <span class="hljs-keyword">in</span> enumerate(kf.split(X_train)):
        <span class="hljs-comment"># create a subset of training and validation datasets from the entire training data</span>
        X_train_fold, X_val_fold = X_train.iloc[train_index], X_train.iloc[val_index]
        y_train_fold, y_val_fold = y_train.iloc[train_index], y_train.iloc[val_index]

        <span class="hljs-comment"># reconstruct a model</span>
        model = base_model(**hparams)

        <span class="hljs-comment"># start the cross validation</span>
        best_val_mse = float(<span class="hljs-string">'inf'</span>)
        patience_counter = <span class="hljs-number">0</span>
        best_model_state = <span class="hljs-literal">None</span>
        best_iteration = <span class="hljs-number">0</span>

        <span class="hljs-keyword">for</span> iteration <span class="hljs-keyword">in</span> range(max_iters):
            <span class="hljs-comment"># train on a subset of the training data</span>
            <span class="hljs-keyword">try</span>:
                model.train_one_step(X_train_fold, y_train_fold, iteration)
            <span class="hljs-keyword">except</span>:
                model.fit(X_train_fold, y_train_fold)

            <span class="hljs-comment"># make a prediction on validation data </span>
            y_pred_val_kf = model.predict(X_val_fold)

            <span class="hljs-comment"># compute validation loss (MSE)</span>
            current_val_mse = mean_squared_error(y_val_fold, y_pred_val_kf)

            <span class="hljs-comment"># check if epochs should be stopped (early stopping)</span>
           <span class="hljs-keyword">if</span> current_val_mse &lt; best_val_mse:
                best_val_mse = current_val_mse
                patience_counter = <span class="hljs-number">0</span>
                best_model_state = model.get_params()
                best_iteration = iteration
           <span class="hljs-keyword">else</span>:
                patience_counter += <span class="hljs-number">1</span>

           <span class="hljs-comment"># execute early stopping when patience_counter exceeds early_stopping_rounds</span>
           <span class="hljs-keyword">if</span> patience_counter &gt;= early_stopping_rounds:
                main_logger.info(<span class="hljs-string">f"Fold <span class="hljs-subst">{fold}</span>: Early stopping triggered at iteration <span class="hljs-subst">{iteration}</span> (best at <span class="hljs-subst">{best_iteration}</span>). Best MSE: <span class="hljs-subst">{best_val_mse:<span class="hljs-number">.4</span>f}</span>"</span>)
                <span class="hljs-keyword">break</span>


        <span class="hljs-comment"># after training epochs, reconstruct the best performing model </span>
        <span class="hljs-keyword">if</span> best_model_state: model.set_params(**best_model_state)

        <span class="hljs-comment"># make prediction</span>
        y_pred_val_kf = model.predict(X_val_fold)

        <span class="hljs-comment"># add MSEs</span>
        mses += mean_squared_error(y_pred_val_kf, y_val_fold)

    <span class="hljs-comment"># compute the final loss (avarage of MSEs across folds)</span>
    ave_mse = mses / n_splits
    <span class="hljs-keyword">return</span> ave_mse
</code></pre>
<p>Then, for the <strong>tuning script</strong>, we use the <code>gp_minimize</code> function from the <code>Scikit-Optimize</code> library.</p>
<p>The <code>gp_minimize</code> function is used to tune hyperparameters with Bayesian optimization.</p>
<p>This function intelligently searches the best hyperparameter set that can minimize the model's error, which is calculated using the <code>run_kfold_validation</code> function defined earlier.</p>
<p>The best-performing hyperparameters are then used to reconstruct and train the final model.</p>
<p><code>src/model/sklearn_model/scripts/tuning.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> functools <span class="hljs-keyword">import</span> partial
<span class="hljs-keyword">from</span> skopt <span class="hljs-keyword">import</span> gp_minimize


<span class="hljs-comment"># define the objective function for Bayesian Optimization using Scikit-Optimize</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">objective</span>(<span class="hljs-params">params, X_train, y_train, base_model, hparam_names</span>):</span>
    hparams = {item: params[i] <span class="hljs-keyword">for</span> i, item <span class="hljs-keyword">in</span> enumerate(hparam_names)}
    ave_mse = run_kfold_validation(X_train=X_train, y_train=y_train, base_model=base_model, hparams=hparams)
    <span class="hljs-keyword">return</span> ave_mse

<span class="hljs-comment"># create the search space</span>
hparam_names = [s.name <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> space]
objective_partial = partial(objective, X_train=X_train, y_train=y_train, base_model=base_model, hparam_names=hparam_names)

<span class="hljs-comment"># search the optimal hyperparameters</span>
results = gp_minimize(
    func=objective_partial,
    dimensions=space,
    n_calls=n_calls,
    random_state=<span class="hljs-number">42</span>,
    verbose=<span class="hljs-literal">False</span>,
    n_initial_points=<span class="hljs-number">10</span>,
)
<span class="hljs-comment"># results</span>
best_hparams = dict(zip(hparam_names, results.x))
best_mse = results.fun

<span class="hljs-comment"># reconstruct the model with the best hyperparameters</span>
best_model = base_model(**best_hparams)

<span class="hljs-comment"># retrain the model with full training dataset</span>
best_model.fit(X_train, y_train)
</code></pre>
<h3 id="heading-step-2-configure-featuremodel-stores-in-s3">Step 2: Configure Feature/Model Stores in S3</h3>
<p>The trained models and processed data are stored in the S3 bucket as a <strong>Parquet file</strong>.</p>
<p>We’ll draft the <code>s3_upload</code> function where the <strong>Boto3 client</strong>, a low-level interface to an AWS service, initiates the connection to S3:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> boto3
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> main_logger

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">s3_upload</span>(<span class="hljs-params">file_path: str</span>):</span>
    <span class="hljs-comment"># initiate the boto3 client</span>
    load_dotenv(override=<span class="hljs-literal">True</span>)
    S3_BUCKET_NAME = os.environ.get(<span class="hljs-string">'S3_BUCKET_NAME'</span>) <span class="hljs-comment"># the bucket created in s3</span>
    s3_client = boto3.client(<span class="hljs-string">'s3'</span>, region_name=os.environ.get(<span class="hljs-string">'AWS_REGION_NAME'</span>)) <span class="hljs-comment"># your default region</span>

    <span class="hljs-keyword">if</span> s3_client:
        <span class="hljs-comment"># create s3 key and upload the file to the bucket</span>
        s3_key = file_path <span class="hljs-keyword">if</span> file_path[<span class="hljs-number">0</span>] != <span class="hljs-string">'/'</span> <span class="hljs-keyword">else</span> file_path[<span class="hljs-number">1</span>:]
        s3_client.upload_file(file_path, S3_BUCKET_NAME, s3_key)
        main_logger.info(<span class="hljs-string">f"file uploaded to s3://<span class="hljs-subst">{S3_BUCKET_NAME}</span>/<span class="hljs-subst">{s3_key}</span>"</span>)
    <span class="hljs-keyword">else</span>:
        main_logger.error(<span class="hljs-string">'failed to create an S3 client.'</span>)
</code></pre>
<h4 id="heading-model-store">Model Store</h4>
<p>Trained PyTorch models are serialized (converted) into <code>.pth</code> files.</p>
<p>Then, these files are uploaded to the S3 bucket, enabling the system to load the trained model when it performs inference in production.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> torch

<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> s3_upload

<span class="hljs-comment"># model serialization, store in local</span>
torch.save(trained_model.state_dict(), MODEL_FILE_PATH)

<span class="hljs-comment"># upload to s3 model store</span>
s3_upload(file_path=MODEL_FILE_PATH)
</code></pre>
<h4 id="heading-feature-store">Feature Store</h4>
<p>The processed data is converted into a CSV and Parquet file format.</p>
<p>Then, the Parquet files are uploaded to the S3 bucket, enabling the system to load the lightweight data when it creates prediction data to perform inference in production.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> s3_upload

<span class="hljs-comment"># store csv and parquet files in local</span>
df.to_csv(file_path, index=<span class="hljs-literal">False</span>)
df.to_parquet(DATA_FILE_PATH, index=<span class="hljs-literal">False</span>)

<span class="hljs-comment"># store in s3 feature store</span>
s3_upload(file_path=DATA_FILE_PATH)

<span class="hljs-comment"># trained preprocessor is also stored to transform the prediction data</span>
s3_upload(file_path=PROCESSOR_PATH)
</code></pre>
<h3 id="heading-step-3-create-a-flask-application-with-api-endpoints">Step 3: Create a Flask Application with API Endpoints</h3>
<p>Next, we’ll create a Flask application with API endpoints.</p>
<p>Flask needs to configure Python scripts in the <code>app.py</code> file located at the root of the project repository.</p>
<p>As showed in the code snippets, the <code>app.py</code> file needs to contain the components in order of:</p>
<ol>
<li><p>AWS Boto3 client setup,</p>
</li>
<li><p>Flask app configuration and API endpoint setup,</p>
</li>
<li><p>Loading the trained preprocessor, processed input data <code>X_test</code>, and trained models,</p>
</li>
<li><p>Invoke the Lambda function via API Gateway, and</p>
</li>
<li><p>The local test section.</p>
</li>
</ol>
<p>Note that <code>X_test</code> should never be used during model training to avoid data leakage.</p>
<p><code>app.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask
<span class="hljs-keyword">from</span> flask_cors <span class="hljs-keyword">import</span> cross_origin
<span class="hljs-keyword">from</span> waitress <span class="hljs-keyword">import</span> serve
<span class="hljs-keyword">from</span> dotenv <span class="hljs-keyword">import</span> load_dotenv

<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> main_logger

<span class="hljs-comment"># global variables (will be loaded from the S3 buckets)</span>
_redis_client = <span class="hljs-literal">None</span>
X_test = <span class="hljs-literal">None</span>
preprocessor = <span class="hljs-literal">None</span>
model = <span class="hljs-literal">None</span>
backup_model = <span class="hljs-literal">None</span>

<span class="hljs-comment"># load env if local else skip (lambda refers to env in production)</span>
AWS_LAMBDA_RUNTIME_API = os.environ.get(<span class="hljs-string">'AWS_LAMBDA_RUNTIME_API'</span>, <span class="hljs-literal">None</span>)
<span class="hljs-keyword">if</span> AWS_LAMBDA_RUNTIME_API <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>: load_dotenv(override=<span class="hljs-literal">True</span>)


<span class="hljs-comment">#### &lt;---- 1. AWS BOTO3 CLIENT ----&gt;</span>
<span class="hljs-comment"># boto3 client </span>
S3_BUCKET_NAME = os.environ.get(<span class="hljs-string">'S3_BUCKET_NAME'</span>, <span class="hljs-string">'ml-sales-pred'</span>)
s3_client = boto3.client(<span class="hljs-string">'s3'</span>, region_name=os.environ.get(<span class="hljs-string">'AWS_REGION_NAME'</span>, <span class="hljs-string">'us-east-1'</span>))
<span class="hljs-keyword">try</span>:
    <span class="hljs-comment"># test connection to boto3 client</span>
    sts_client = boto3.client(<span class="hljs-string">'sts'</span>)
    identity = sts_client.get_caller_identity()
    main_logger.info(<span class="hljs-string">f"Lambda is using role: <span class="hljs-subst">{identity[<span class="hljs-string">'Arn'</span>]}</span>"</span>)
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    main_logger.error(<span class="hljs-string">f"Lambda credentials/permissions error: <span class="hljs-subst">{e}</span>"</span>)

<span class="hljs-comment">#### &lt;---- 2. FLASK CONFIGURATION &amp; API ENDPOINTS ----&gt;</span>
<span class="hljs-comment"># configure the flask app</span>
app = Flask(__name__)
app.config[<span class="hljs-string">'CORS_HEADERS'</span>] = <span class="hljs-string">'Content-Type'</span>

<span class="hljs-comment"># add a simple API endpoint to serve the prediction by price point to test</span>
<span class="hljs-meta">@app.route('/v1/predict-price/&lt;string:stockcode&gt;', methods=['GET', 'OPTIONS'])</span>
<span class="hljs-meta">@cross_origin(origins=origins, methods=['GET', 'OPTIONS'], supports_credentials=True)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">predict_price</span>(<span class="hljs-params">stockcode</span>):</span>
    df_stockcode = <span class="hljs-literal">None</span>

    <span class="hljs-comment"># fetch request params</span>
    data = request.args.to_dict()

    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># fetch cache</span>
        <span class="hljs-keyword">if</span> _redis_client <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
            <span class="hljs-comment"># returns cached prediction results if any without performing inference</span>
            cached_prediction_result = _redis_client.get(cache_key_prediction_result_by_stockcode)
            <span class="hljs-keyword">if</span> cached_prediction_result: 
                <span class="hljs-keyword">return</span> jsonify(json.loads(json.dumps(cached_prediction_result)))

            <span class="hljs-comment"># historical data of the selected product</span>
            cached_df_stockcode = _redis_client.get(cache_key_df_stockcode)
            <span class="hljs-keyword">if</span> cached_df_stockcode: df_stockcode = json.loads(json.dumps(cached_df_stockcode))


        <span class="hljs-comment"># define the price range to make predictions. can be a request param, or historical min/max prices</span>
        min_price = float(data.get(<span class="hljs-string">'unitprice_min'</span>, df_stockcode[<span class="hljs-string">'unitprice_min'</span>][<span class="hljs-number">0</span>]))
        max_price = float(data.get(<span class="hljs-string">'unitprice_max'</span>, df_stockcode[<span class="hljs-string">'unitprice_max'</span>][<span class="hljs-number">0</span>]))

        <span class="hljs-comment"># create bins in the price range. when the number of the bins increase, the prediction becomes more smooth, but requires more computational cost</span>
        NUM_PRICE_BINS = int(data.get(<span class="hljs-string">'num_price_bins'</span>, <span class="hljs-number">100</span>))
        price_range = np.linspace(min_price, max_price, NUM_PRICE_BINS)

        <span class="hljs-comment"># create a prediction dataset by merging X_test (dataset never used in model training) and df_stockcode</span>
        price_range_df = pd.DataFrame({ <span class="hljs-string">'unitprice'</span>: price_range })
        test_sample = X_test.sample(n=<span class="hljs-number">1000</span>, random_state=<span class="hljs-number">42</span>)
        test_sample_merged = test_sample.merge(price_range_df, how=<span class="hljs-string">'cross'</span>) <span class="hljs-keyword">if</span> X_test <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">else</span> price_range_df
        test_sample_merged.drop(<span class="hljs-string">'unitprice_x'</span>, axis=<span class="hljs-number">1</span>, inplace=<span class="hljs-literal">True</span>)
        test_sample_merged.rename(columns={<span class="hljs-string">'unitprice_y'</span>: <span class="hljs-string">'unitprice'</span>}, inplace=<span class="hljs-literal">True</span>)

        <span class="hljs-comment"># preprocess the dataset</span>
        X = preprocessor.transform(test_sample_merged) <span class="hljs-keyword">if</span> preprocessor <span class="hljs-keyword">else</span> test_sample_merged

        <span class="hljs-comment"># perform inference</span>
        y_pred_actual = <span class="hljs-literal">None</span>
        epsilon = <span class="hljs-number">0</span>
        <span class="hljs-comment"># try using the primary model</span>
        <span class="hljs-keyword">if</span> model:
            input_tensor = torch.tensor(X, dtype=torch.float32)
            model.eval()
            <span class="hljs-keyword">with</span> torch.inference_mode():
                y_pred = model(input_tensor)
                y_pred = y_pred.cpu().numpy().flatten()
                y_pred_actual = np.exp(y_pred + epsilon)

        <span class="hljs-comment"># if not, use backups</span>
        <span class="hljs-keyword">elif</span> backup_model:
            y_pred = backup_model.predict(X)
            y_pred_actual = np.exp(y_pred + epsilon)


        <span class="hljs-comment"># finalize the outcome for client app</span>
        df_ = test_sample_merged.copy()
        df_[<span class="hljs-string">'quantity'</span>] = np.floor(y_pred_actual) <span class="hljs-comment"># quantity must be an integer</span>
        df_[<span class="hljs-string">'sales'</span>] = df_[<span class="hljs-string">'quantity'</span>] * df_[<span class="hljs-string">'unitprice'</span>] <span class="hljs-comment"># compute sales</span>
        df_ = df_.sort_values(by=<span class="hljs-string">'unitprice'</span>)

        <span class="hljs-comment"># aggregate the results by the unitprice in the price range</span>
        df_results = df_.groupby(<span class="hljs-string">'unitprice'</span>).agg(
            quantity=(<span class="hljs-string">'quantity'</span>, <span class="hljs-string">'median'</span>),
            quantity_min=(<span class="hljs-string">'quantity'</span>, <span class="hljs-string">'min'</span>),
            quantity_max=(<span class="hljs-string">'quantity'</span>, <span class="hljs-string">'max'</span>),
            sales=(<span class="hljs-string">'sales'</span>, <span class="hljs-string">'median'</span>),
        ).reset_index()

        <span class="hljs-comment"># find the optimal price point</span>
        optimal_row = df_results.loc[df_results[<span class="hljs-string">'sales'</span>].idxmax()]
        optimal_price = optimal_row[<span class="hljs-string">'unitprice'</span>]
        optimal_quantity = optimal_row[<span class="hljs-string">'quantity'</span>]
        best_sales = optimal_row[<span class="hljs-string">'sales'</span>]

        all_outputs = []
        <span class="hljs-keyword">for</span> _, row <span class="hljs-keyword">in</span> df_results.iterrows():
            current_output = {
                <span class="hljs-string">"stockcode"</span>: stockcode,
                <span class="hljs-string">"unit_price"</span>: float(row[<span class="hljs-string">'unitprice'</span>]),
                <span class="hljs-string">'quantity'</span>: int(row[<span class="hljs-string">'quantity'</span>]),
                <span class="hljs-string">'quantity_min'</span>: int(row[<span class="hljs-string">'quantity_min'</span>]),
                <span class="hljs-string">'quantity_max'</span>: int(row[<span class="hljs-string">'quantity_max'</span>]),
                <span class="hljs-string">"predicted_sales"</span>: float(row[<span class="hljs-string">'sales'</span>]),
            }
            all_outputs.append(current_output)

        <span class="hljs-comment"># store the prediction results in cache</span>
        <span class="hljs-keyword">if</span> all_outputs <span class="hljs-keyword">and</span> _redis_client <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
             serialized_data = json.dumps(all_outputs)
            _redis_client.set(
                cache_key_prediction_result_by_stockcode, 
                serialized_data,
                ex=<span class="hljs-number">3600</span>     <span class="hljs-comment"># expire in an hour</span>
            )

        <span class="hljs-comment"># return a list of all outputs</span>
        <span class="hljs-keyword">return</span> jsonify(all_outputs)

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e: <span class="hljs-keyword">return</span> jsonify([])


<span class="hljs-comment"># request header management (for the process from API gateway to the Lambda)</span>
<span class="hljs-meta">@app.after_request</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_header</span>(<span class="hljs-params">response</span>):</span>
    response.headers[<span class="hljs-string">'Cache-Control'</span>] = <span class="hljs-string">'public, max-age=0'</span>
    response.headers[<span class="hljs-string">'Access-Control-Allow-Origin'</span>] = CLIENT_A
    response.headers[<span class="hljs-string">'Access-Control-Allow-Headers'</span>] = <span class="hljs-string">'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Origin'</span>
    response.headers[<span class="hljs-string">'Access-Control-Allow-Methods'</span>] = <span class="hljs-string">'GET, POST, OPTIONSS'</span>
    response.headers[<span class="hljs-string">'Access-Control-Allow-Credentials'</span>] = <span class="hljs-string">'true'</span>
    <span class="hljs-keyword">return</span> response

<span class="hljs-comment">#### &lt;---- 3. LOADING PROCESSOR, DATASET, AND MODELS ----&gt;</span>
load_processor()
load_x_test()
load_model()

<span class="hljs-comment">#### &lt;---- 4. INVOKE LAMBDA ----&gt;</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handler</span>(<span class="hljs-params">event, context</span>):</span>
    logger.info(<span class="hljs-string">"lambda handler invoked."</span>)
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># connecting the redis client after the lambda is invoked</span>
        get_redis_client()
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        logger.critical(<span class="hljs-string">f"failed to establish initial Redis connection in handler: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">'statusCode'</span>: <span class="hljs-number">500</span>,
            <span class="hljs-string">'body'</span>: json.dumps({<span class="hljs-string">'error'</span>: <span class="hljs-string">'Failed to initialize Redis client. Check environment variables and network config.'</span>})
        }

    <span class="hljs-comment"># use the awsgi package to convert JSON to WSGI</span>
    <span class="hljs-keyword">return</span> awsgi.response(app, event, context)


<span class="hljs-comment">#### &lt;---- 5. FOR LOCAL TEST ----&gt;</span>
<span class="hljs-comment"># serve the application locally on WSGI server, waitress</span>
<span class="hljs-comment"># lambda will ignore this section.</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:   
    <span class="hljs-keyword">if</span> os.getenv(<span class="hljs-string">'ENV'</span>) == <span class="hljs-string">'local'</span>:
        main_logger.info(<span class="hljs-string">"...start the operation (local)..."</span>)
        serve(app, host=<span class="hljs-string">'0.0.0.0'</span>, port=<span class="hljs-number">5002</span>)
    <span class="hljs-keyword">else</span>:
        app.run(host=<span class="hljs-string">'0.0.0.0'</span>, port=<span class="hljs-number">8080</span>)
</code></pre>
<p>I’ll test the endpoint locally using the <code>uv</code> package manager:</p>
<pre><code class="lang-python">$uv run app.py --cache-clear

$curl http://localhost:<span class="hljs-number">5002</span>/v1/predict-price/{STOCKCODE}
</code></pre>
<p>The system provided a list of sales predictions for each price point:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755607075000/e0e8cbcb-8817-4aa5-b3d1-37b76cc684fb.png" alt="Fig. Screenshot of the Flask app local response" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Fig. Screenshot of the Flask app local response</p>
<h4 id="heading-key-points-on-flask-app-configuration">Key Points on Flask App Configuration</h4>
<p>There are various points you should take into consideration when configuring a Flask application with Lambda. Let’s go over them now:</p>
<h5 id="heading-1-a-few-api-endpoints-per-container"><strong>1. A Few API Endpoints Per Container</strong></h5>
<p>Adding many API endpoints to a single serverless instance can lead to <strong>monolithic function concern</strong> where issues in one endpoint impact others.</p>
<p>In this project, we’ll focus on a single endpoint per container – and if needed, we can add separate Lambda functions to the system.</p>
<h5 id="heading-2-understanding-the-handler-function-and-the-role-of-awsgi"><strong>2. Understanding the</strong> <code>handler</code> <strong>Function and the role of AWSGI</strong></h5>
<p>The <code>handler</code> function is invoked every time the Lambda function receives a client request from the API Gateway.</p>
<p>The function takes the <code>event</code> argument that includes the request details in a <strong>JSON dictionary</strong> and passes it to the Flask application.</p>
<p><strong>AWSGI</strong> acts as an adapter, translating a Lambda event in JSON format into a WSGI request that a Flask application can understand, and converts the application’s response back into a JSON format that Lambda and API Gateway can process.</p>
<h5 id="heading-3-using-cache-storage"><strong>3. Using Cache Storage</strong></h5>
<p>The <code>get_redis_client</code> function is called once the <code>handler</code> function is called by the API Gateway. This allows the Flask application to store or fetch a cache from the Redis client:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> redis
<span class="hljs-keyword">import</span> redis.cluster
<span class="hljs-keyword">from</span> redis.cluster <span class="hljs-keyword">import</span> ClusterNode

_redis_client = <span class="hljs-literal">None</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_redis_client</span>():</span>
    <span class="hljs-keyword">global</span> _redis_client
    <span class="hljs-keyword">if</span> _redis_client <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        REDIS_HOST = os.environ.get(<span class="hljs-string">"REDIS_HOST"</span>)
        REDIS_PORT = int(os.environ.get(<span class="hljs-string">"REDIS_PORT"</span>, <span class="hljs-number">6379</span>))
        REDIS_TLS = os.environ.get(<span class="hljs-string">"REDIS_TLS"</span>, <span class="hljs-string">"true"</span>).lower() == <span class="hljs-string">"true"</span>
        <span class="hljs-keyword">try</span>:
            startup_nodes = [ClusterNode(host=REDIS_HOST, port=REDIS_PORT)]
            _redis_client = redis.cluster.RedisCluster(
                startup_nodes=startup_nodes,
                decode_responses=<span class="hljs-literal">True</span>,
                skip_full_coverage_check=<span class="hljs-literal">True</span>,
                ssl=REDIS_TLS,                  <span class="hljs-comment"># elasticache has encryption in transit: enabled -&gt; must be true</span>
                ssl_cert_reqs=<span class="hljs-literal">None</span>,
                socket_connect_timeout=<span class="hljs-number">5</span>,
                socket_timeout=<span class="hljs-number">5</span>,
                health_check_interval=<span class="hljs-number">30</span>,
                retry_on_timeout=<span class="hljs-literal">True</span>,
                retry_on_error=[
                    redis.exceptions.ConnectionError,
                    redis.exceptions.TimeoutError
                ],
                max_connections=<span class="hljs-number">10</span>,            <span class="hljs-comment"># limit connections for Lambda</span>
                max_connections_per_node=<span class="hljs-number">2</span>     <span class="hljs-comment"># limit per node</span>
            )
            _redis_client.ping()
            main_logger.info(<span class="hljs-string">"successfully connected to ElastiCache Redis Cluster (Configuration Endpoint)"</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            main_logger.error(<span class="hljs-string">f"an unexpected error occurred during Redis Cluster connection: <span class="hljs-subst">{e}</span>"</span>, exc_info=<span class="hljs-literal">True</span>)
            _redis_client = <span class="hljs-literal">None</span>
    <span class="hljs-keyword">return</span> _redis_client
</code></pre>
<h5 id="heading-4-handling-heavy-tasks-outside-of-the-handler-function"><strong>4. Handling Heavy Tasks Outside of the</strong> <code>handler</code> <strong>Function</strong></h5>
<p>Serverless functions can experience a <strong>cold start duration</strong>.</p>
<p>While a Lambda function can run for up to 15 minutes, its associated API Gateway has a timeout of 29 seconds (29,000 ms) for a RESTful API.</p>
<p>So, any heavy tasks like loading preprocessors, input data, or models should be performed once outside of the <code>handler</code> function, ensuring they are ready <em>before</em> the API endpoint is called.</p>
<p>Here are the loading functions called in <code>app.py</code>.</p>
<p><code>app.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> joblib

<span class="hljs-keyword">from</span> src._utils <span class="hljs-keyword">import</span> s3_load, s3_load_to_temp_file

preprocessor = <span class="hljs-literal">None</span>
X_test = <span class="hljs-literal">None</span>
model = <span class="hljs-literal">None</span>
backup_model = <span class="hljs-literal">None</span>


<span class="hljs-comment"># load processor</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">load_preprocessor</span>():</span>
    <span class="hljs-keyword">global</span> preprocessor
    preprocessor_tempfile_path = s3_load_to_temp_file(PREPROCESSOR_PATH)
    preprocessor = joblib.load(preprocessor_tempfile_path)
    os.remove(preprocessor_tempfile_path)


<span class="hljs-comment"># load input data</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">load_x_test</span>():</span>
    <span class="hljs-keyword">global</span> X_test
    x_test_io = s3_load(file_path=X_TEST_PATH)
    X_test = pd.read_parquet(x_test_io)


<span class="hljs-comment"># load model</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">load_model</span>():</span>
    <span class="hljs-keyword">global</span> model, backup_model
    <span class="hljs-comment"># try loading &amp; reconstructing the primary model</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># first load io file from the s3 bucket</span>
        model_data_bytes_io_ = s3_load(file_path=DFN_FILE_PATH)
        <span class="hljs-comment"># convert to checkpoint dictionary (containing hyperparameter set)</span>
        checkpoint_ = torch.load(
            model_data_bytes_io_, 
            weights_only=<span class="hljs-literal">False</span>, 
            map_location=device
        )
        <span class="hljs-comment"># reconstruct the model</span>
        model = t.scripts.load_model(checkpoint=checkpoint_, file_path=DFN_FILE_PATH)
        <span class="hljs-comment"># set the model evaluation mode</span>
        model.eval()

    <span class="hljs-comment"># else, backup model</span>
     <span class="hljs-keyword">except</span>:
        load_artifacts_backup_model()
</code></pre>
<h3 id="heading-step-4-publish-a-docker-image-to-ecr">Step 4: Publish a Docker Image to ECR</h3>
<p>After configuring the Flask application, we’ll containerize the entire application on <strong>Docker</strong>.</p>
<p>Containerization makes a package of the application, including models, its dependencies, and configuration in machine learning context, as a container<strong>.</strong></p>
<p>Docker creates a container image based on the instructions defined in a Dockerfile, and the Docker engine uses the image to run the isolated container.</p>
<p>In this project, we’ll upload the Docker container image to ECR, so the Lambda function can access it in production.</p>
<p>After this, we’ll define the <code>.dockerignore</code> file to optimize the container image:</p>
<p><code>.dockerignore</code></p>
<pre><code class="lang-plaintext"># any irrelevant data
__pycache__/
.ruff_cache/
.DS_Store/
.venv/
dist/
.vscode
*.psd
*.pdf
[a-f]*.log
tmp/
awscli-bundle/

# add any experimental models, unnecessary data
dfn_bayesian/
dfn_grid/
data/
notebooks/
</code></pre>
<p><code>Dockerfile</code></p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># serve from aws ecr </span>
<span class="hljs-keyword">FROM</span> public.ecr.aws/lambda/python:<span class="hljs-number">3.12</span>

<span class="hljs-comment"># define a working directory in the container</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># copy the entire repository (except .dockerignore) into the container at /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . /app/</span>

<span class="hljs-comment"># install dependencies defined in the requirements.txt</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install --no-cache-dir -r requirements.txt</span>

<span class="hljs-comment"># define commands</span>
<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [ <span class="hljs-string">"python"</span> ]</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"-m"</span>, <span class="hljs-string">"awslambdaric"</span>, <span class="hljs-string">"app.handler"</span> ]</span>
</code></pre>
<h4 id="heading-test-in-local">Test in Local</h4>
<p>Next, we’ll test the Docker image by building the container named <code>my-app</code> locally:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$docker</span> build -t my-app -f Dockerfile .
</code></pre>
<p>Then, we’ll run the container with the <code>waitress</code> server in local:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$docker</span> run -p 5002:5002 -e ENV=<span class="hljs-built_in">local</span> my-app app.py
</code></pre>
<p>The <code>-e ENV=local</code> flag sets the environment variable inside the container, which will trigger the <code>waitress.serve()</code> call in the <code>app.py</code>.</p>
<p>In the terminal, you’ll find a message saying the following:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1260/0*zu8mamgKMKOUxwCA.png" alt="Flask app response" width="600" height="400" loading="lazy"></p>
<p>You can also call the endpoint created to see the results returned:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$uv</span> run app.py --cache-clear

<span class="hljs-variable">$curl</span> http://localhost:5002/v1/predict-price/{STOCKCODE}
</code></pre>
<h4 id="heading-publish-the-docker-image-to-ecr">Publish the Docker Image to ECR</h4>
<p>To publish the Docker image, we first need to configure the default AWS credentials and region:</p>
<ul>
<li><p>From the AWS account console, issue an access token and check the default region.</p>
</li>
<li><p>Store them in the <code>~/aws/credentials</code> and <code>~/aws/config</code> files:</p>
</li>
</ul>
<p><code>~/aws/credentials</code></p>
<pre><code class="lang-plaintext">[default] 
aws_secret_access_key=
aws_access_key_id=
</code></pre>
<p><code>~/aws/config</code></p>
<pre><code class="lang-plaintext">[default]
region=
</code></pre>
<p>After the configuration, we’ll publish the Docker image to ECR.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># authenticate the docker client to ECR</span>
<span class="hljs-variable">$aws</span> ecr get-login-password --region &lt;your-aws-region&gt; | docker login --username AWS --password-stdin &lt;your-aws-account-id&gt;.dkr.ecr.&lt;your-aws-region&gt;.amazonaws.com

<span class="hljs-comment"># create repository</span>
<span class="hljs-variable">$aws</span> ecr create-repository --repository-name &lt;your-repo-name&gt; --region &lt;your-aws-region&gt;

<span class="hljs-comment"># tag the docker image</span>
<span class="hljs-variable">$docker</span> tag &lt;your-repo-name&gt;:&lt;your-app-version&gt;  &lt;your-aws-account-id&gt;.dkr.ecr.&lt;your-aws-region&gt;.amazonaws.com/&lt;your-app-name&gt;:&lt;your-app-version&gt;

<span class="hljs-comment"># push</span>
<span class="hljs-variable">$docker</span> push &lt;your-aws-account-id&gt;.dkr.ecr.&lt;your-aws-region&gt;.amazonaws.com/&lt;your-repo-name&gt;:&lt;your-app-version&gt;
</code></pre>
<p>Here’s what’s going on:</p>
<ul>
<li><p><code>&lt;your-aws-region&gt;</code>: Your default AWS region (for example, <code>us-east-1</code> ).</p>
</li>
<li><p><code>&lt;your-aws-account-id&gt;</code>: 12-digit AWS account ID.</p>
</li>
<li><p><code>&lt;your-repo-name&gt;</code>: Your desired repository name.</p>
</li>
<li><p><code>&lt;your-app-version&gt;</code>: Your desired tag name (for example, <code>v1.0</code>).</p>
</li>
</ul>
<p>Now, the Docker image is stored in ECR with the tag:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1260/0*tUQkbDW-uAmrjBfx.png" alt="Fig. Screenshot of the AWS ECR console" width="600" height="400" loading="lazy"></p>
<p>Fig. Screenshot of the AWS ECR console</p>
<h3 id="heading-step-5-create-a-lambda-function">Step 5: Create a Lambda Function</h3>
<p>Next, we’ll create a Lambda function.</p>
<p>From the Lambda console, choose:</p>
<ul>
<li><p>The <code>Container Image</code> option,</p>
</li>
<li><p>The container image URL from the pull down list,</p>
</li>
<li><p>A function name of our choice, and</p>
</li>
<li><p>An architecture type (arm64 is recommended for a better price-performance).</p>
</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:1260/0*3b-wIEUzRooQcvN_.png" alt="Fig. Screenshot of AWS Lambda function configurationFig. Screenshot of AWS Lambda function configuration" width="600" height="400" loading="lazy"></p>
<p>Fig. Screenshot of AWS Lambda function configuration</p>
<p>The Lambda function <code>my-app</code> was successfully launched.</p>
<h4 id="heading-connect-the-lambda-function-to-api-gateway">Connect the Lambda function to API Gateway</h4>
<p>Next, we’ll add API gateway as an event trigger to the Lambda function.</p>
<p>First, visit the API Gateway console and create <strong>REST API methods</strong> using the ARN of the Lambda function (press enter or click to view image in full size):</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1260/0*60TP64gdSjhKfiO8.png" alt="Fig. Screenshot of the AWS API Gateway configurationFig. Screenshot of the AWS API Gateway configuration" width="600" height="400" loading="lazy"></p>
<p>Fig. Screenshot of the AWS API Gateway configuration</p>
<p>Then, add resources to the created API gateway to create an endpoint:<br><code>API Gateway &gt; APIs &gt; Resources &gt; Create Resource</code></p>
<ul>
<li><p>Align the resource endpoint with the API endpoint defined in the <a target="_blank" href="http://app.py"><code>app.py</code></a>.</p>
</li>
<li><p>Configure CORS (for example, accept specific origins).</p>
</li>
<li><p>Deploy the resource to the stage.</p>
</li>
</ul>
<p>Going back to the Lambda console, you’ll find the API Gateway is connected as an event trigger:<br><code>Lambda &gt; Function &gt; my-app (your function name)</code></p>
<p><img src="https://miro.medium.com/v2/resize:fit:1260/0*DlfiEieZArmYlOuT.png" alt="Fig. Screenshot of the AWS Lambda dashboard" width="600" height="400" loading="lazy"></p>
<p>Fig. Screenshot of the AWS Lambda dashboard</p>
<h3 id="heading-step-6-configure-aws-resources">Step 6: Configure AWS Resources</h3>
<p>Lastly, we’ll configure the related AWS resources to make the system work in production.</p>
<p>This process involves the following steps:</p>
<h4 id="heading-1-the-iam-role-controls-who-to-access-resources">1. The IAM Role: Controls Who to Access Resources</h4>
<p>AWS requires <strong>IAM roles</strong> to grant temporary, secure permissions to users, mitigating security risks related to long-term credentials like passwords.</p>
<p>The IAM role leverages policies to grant accesses to the selected service. Policies can be issued by AWS or customized by the user by defining the inline policy.</p>
<p>It is important to avoid overly permissive access rights for the IAM role.</p>
<ol>
<li><p>In the Lambda function console, check the execution role:<br> <code>Lambda &gt; Function &gt; &lt;FUNCTION&gt; &gt; Permission &gt; The execution role</code>.</p>
</li>
<li><p>Set up the following policies to allow the Lambda’s IAM role to handle necessary operations:</p>
<ul>
<li><p><strong>Lambda</strong> <code>AWSLambdaExecute</code>: Allows executing the function.</p>
</li>
<li><p><strong>EC2</strong> <code>Inline policy</code>: Allows controlling the security group and the VPC of the Lambda function.</p>
</li>
<li><p><strong>ECR</strong> <code>AmazonElasticContainerRegistryPublicFullAccess</code> + <code>Inline policy</code>: Allows storing and pulling the Docker image.</p>
</li>
<li><p><strong>ElastiCache</strong> <code>AmazonElastiCacheFullAccess</code> + <code>Inline policy</code>: Allows storing and pulling caches.</p>
</li>
<li><p><strong>S3</strong>: <code>AmazonS3ReadOnlyAccess</code> + <code>Inline policy</code>: Allows reading and storing contents.</p>
</li>
</ul>
</li>
</ol>
<p>Now, the IAM role can access these resources and perfo the allowed actions.</p>
<h4 id="heading-2-the-security-group-controls-network-traffic">2. The Security Group: Controls Network Traffic</h4>
<p>A <strong>security group</strong> is a virtual firewall that controls inbound and outbound network traffic for AWS resources.</p>
<p>It uses stateful (allowing return traffic automatically) “allow-only” rules based on protocol, port, and IP address, where it denies all traffic by default.</p>
<p>Create a new security group for the Lambda function:<br><code>EC2 &gt; Security Groups &gt; &lt;YOUR SECURITY GROUP&gt;</code></p>
<p>Now, we’ll want to setup inbound / outbound traffic rules.</p>
<p>The inbound rules:</p>
<ul>
<li><p><strong>S3 → Lambda</strong>:<strong>Type</strong>*: HTTPS /* <strong>Protocol</strong>*: TCP /* <strong>Port range</strong>*: 443 / Source: Custom**</p>
</li>
<li><p><strong>ElastiCache → Lambda</strong>:<strong>Type</strong>*: Custom TCP /* <strong>Port range</strong>*: 6379 / Source: Custom**</p>
</li>
</ul>
<p>*Choose the created security group for the Lambda function as a custom source.</p>
<p>The outbound rules:</p>
<ul>
<li><p><strong>Lambda → Internet</strong>: <strong>Type</strong>*: HTTPS /* <strong>Protocol</strong>*: TCP /* <strong>Port range</strong>*: 443 /* <strong>Destination</strong>*: 0.0.0.0/0*</p>
</li>
<li><p><strong>ElastiCache → Internet</strong>: <strong>Type</strong>*: All Traffic /* <strong>Destination</strong>*: 0.0.0.0/0*</p>
</li>
</ul>
<h4 id="heading-3-the-virtual-private-cloud-vpc">3. The Virtual Private Cloud (VPC)</h4>
<p>A <strong>Virtual Private Cloud (VPC)</strong> provides a logically isolated private network for the AWS resources, acting as our own private data center within AWS.</p>
<p>AWS can create a <strong>Hyperplane ENI</strong> (Elastic Network Interface) for the Lambda function and its connected resources in the subnets of the VPC.</p>
<p>Though it’s optional, we’ll use the VPC to connect the Lambda function to the S3 storage and ElastiCache.</p>
<p>This process involves:</p>
<ol>
<li><p>Creating a VPC endpoint from the VPC console:<code>VPC &gt; Create VPC</code>.</p>
</li>
<li><p>Creating an STS (Security Token Service) endpoint:<br> <code>VPC &gt; PrivateLink and Lattice &gt; Endpoints &gt; Create Endpoint &gt;</code></p>
<ul>
<li><p><strong>Type</strong>*: AWS Service*</p>
</li>
<li><p><strong>Service name</strong>*: com.amazonaws.&lt;YOUR REGION&gt;.sts*</p>
</li>
<li><p><strong>Type</strong>*: Interface*</p>
</li>
<li><p><strong>VPC:</strong> Select the VPC created earlier.</p>
</li>
<li><p><strong>Subnets</strong>*: Select all subnets.*</p>
</li>
<li><p><strong>Security groups</strong>*: Select the security group of the Lambda function.*</p>
</li>
<li><p><strong>Policy</strong>*: Full access*</p>
</li>
<li><p><strong>Enable DNS names</strong></p>
</li>
</ul>
</li>
</ol>
<p>The VPC must have a dedicated endpoint for STS to receive temporary credentials from STS.</p>
<ol start="3">
<li><p>Create an S3 endpoint in the VPC:<br> <code>VPC &gt; PrivateLink and Lattice &gt; Endpoints &gt; Create Endpoint &gt;</code></p>
<ul>
<li><p><strong>Type</strong>*: AWS Service*</p>
</li>
<li><p><strong>Service name</strong>*: com.amazonaws.&lt;YOUR REGION&gt;.s3*</p>
</li>
<li><p><strong>Type</strong>*: Gateway*</p>
</li>
<li><p><strong>VPC:</strong> Select the VPC created earlier.</p>
</li>
<li><p><strong>Subnets</strong>*: Select all subnets.*</p>
</li>
<li><p><strong>Security groups</strong>*: Select the security group of the Lambda function.*</p>
</li>
<li><p><strong>Policy</strong>*: Full access*</p>
</li>
</ul>
</li>
</ol>
<p>Lastly, check the security group of the Lambda function and ensure that its VPC ID directs to the VPC created: <code>EC2 &gt; Security Group &gt; &lt;YOUR SECURITY GROUP FOR THE LAMDA FUNCTION&gt; &gt; VPC ID</code>.</p>
<p>That’s all for the deployment flow.</p>
<p>We can now test the API endpoint in production. Copy the <strong>Invoke URL</strong> of the deployed API endpoint: <code>API Gateway &gt; APIs &gt; Stages &gt; Invoke URL</code>. Then call the API endpoint and check if it responds predictions:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$curl</span> -H <span class="hljs-string">'Authorization: Bearer YOUR_API_TOKEN'</span> -H <span class="hljs-string">'Accept: application/json'</span> \
     <span class="hljs-string">'&lt;INVOKE URL&gt;/&lt;ENDPOINT&gt;'</span>
</code></pre>
<p>For logging and debugging, we’ll use the LiveTail of CloudWatch: <code>CloudWatch &gt; LiveTail</code>.</p>
<h2 id="heading-building-a-client-application-optional">Building a Client Application (Optional)</h2>
<p>For full-stack deployment, we’ll build a simple React application to display the prediction using the <a target="_blank" href="https://recharts.org/en-US">recharts</a> library for visualization.</p>
<p>Other options for quick frontend deployment include <a target="_blank" href="https://streamlit.io/">Streamlit</a> or <a target="_blank" href="https://www.gradio.app/">Gradio</a>.</p>
<h3 id="heading-the-react-application">The React Application</h3>
<p>The React application creates a web page that fetches and visualizes sales predictions from an external API, recommending an optimal price point.</p>
<p>The app uses <code>useState</code> to manage its data and state, including the selected product, the list of sales predictions, and the loading/error status.</p>
<p>When the user initiates a request, a <code>useEffect</code> hook triggers a <code>fetch</code> request to a Flask backend. It handles the API response as a <strong>data stream</strong>, processing it line by line to progressively update the predictions.</p>
<p>The <code>AreaChart</code> from the <code>recharts</code> library then visualizes this data. The X-axis represents the <code>price</code> and the Y-axis represents the <code>sales</code>. The chart updates in real-time as the data streams in. Finally, the app displays the optimal price once all the predictions are received.</p>
<p><code>App.js</code>: (in a separate React app)</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } <span class="hljs-keyword">from</span> <span class="hljs-string">'recharts'</span>


<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// state</span>
  <span class="hljs-keyword">const</span> [predictions, setPredictions] = useState([])
  <span class="hljs-keyword">const</span> [start, setStart] = useState(<span class="hljs-literal">false</span>)
  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">false</span>)

  <span class="hljs-comment">// product data</span>
  <span class="hljs-keyword">let</span> selectedStockcode = <span class="hljs-string">'85123A'</span>
  <span class="hljs-keyword">let</span> selectedProduct = productOptions.filter(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> item.id === selectedStockcode)[<span class="hljs-number">0</span>]

  <span class="hljs-comment">// api endpoint</span>
  <span class="hljs-keyword">const</span> flaskBackendUrl = <span class="hljs-string">"YOUR FLASK BACKEND URL"</span>

  <span class="hljs-comment">// create chart data to display</span>
  <span class="hljs-keyword">const</span> chartDataSales = predictions &amp;&amp; predictions.length &gt; <span class="hljs-number">0</span>
    ? predictions
      .map(<span class="hljs-function"><span class="hljs-params">item</span> =&gt;</span> ({
        <span class="hljs-attr">price</span>: item.unit_price,
        <span class="hljs-attr">sales</span>: item.predicted_sales,
        <span class="hljs-attr">volume</span>: item.unit_price !== <span class="hljs-number">0</span> ? item.predicted_sales / item.unit_price : <span class="hljs-number">0</span>
      }))
      .sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> a.price - b.price)
    : [...selectedProduct[<span class="hljs-string">'histPrices'</span>]]

  <span class="hljs-comment">// optimal price to display</span>
  <span class="hljs-keyword">const</span> optimalPrice = predictions.length &gt; <span class="hljs-number">0</span>
    ? predictions.sort(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> b.predicted_sales - a.predicted_sales)[<span class="hljs-number">0</span>][<span class="hljs-string">'unit_price'</span>]
    : <span class="hljs-number">0</span>

  <span class="hljs-comment">// fetch prediction results</span>
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> handlePrediction = <span class="hljs-keyword">async</span> () =&gt; {
      setIsLoading(<span class="hljs-literal">true</span>)
      setPredictions([])
      <span class="hljs-keyword">const</span> errorPrices = selectedProduct[<span class="hljs-string">'errorPrices'</span>]

      <span class="hljs-keyword">await</span> fetch(flaskBackendUrl)
        .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> {
          <span class="hljs-keyword">if</span> (res.status !== <span class="hljs-number">200</span>) { setPredictions(errorPrices); setIsLoading(<span class="hljs-literal">false</span>); setStart(<span class="hljs-literal">false</span>) }
          <span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.resolve(res.clone().json())
        })
        .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> {
          <span class="hljs-keyword">if</span> (res &amp;&amp; res.length &gt; <span class="hljs-number">0</span>) setPredictions(res)
          <span class="hljs-keyword">else</span> setPredictions(errorPrices)
          setIsLoading(<span class="hljs-literal">false</span>); setStart(<span class="hljs-literal">false</span>)
        })
        .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> { setPredictions(errorPrices); setIsLoading(<span class="hljs-literal">false</span>); setStart(<span class="hljs-literal">false</span>) })
        .finally(setStart(<span class="hljs-literal">false</span>))
    }

    <span class="hljs-keyword">if</span> (start) handlePrediction()
    <span class="hljs-keyword">if</span> (predictions &amp;&amp; predictions.length &gt; <span class="hljs-number">0</span>) setStart(<span class="hljs-literal">false</span>)
  }, [flaskBackendUrl, start])


  <span class="hljs-comment">// render</span>
  <span class="hljs-keyword">if</span> (isLoading) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Loading</span> /&gt;</span></span>
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ResponsiveContainer</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"100%"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AreaChart</span>
          <span class="hljs-attr">key</span>=<span class="hljs-string">{chartDataSales.length}</span>
          <span class="hljs-attr">data</span>=<span class="hljs-string">{chartDataSales.sort(data</span> =&gt;</span> data.unit_price)}
          margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
        &gt;
          <span class="hljs-tag">&lt;<span class="hljs-name">CartesianGrid</span> <span class="hljs-attr">strokeDasharray</span>=<span class="hljs-string">"3 3"</span> <span class="hljs-attr">strokeOpacity</span>=<span class="hljs-string">{0.6}</span> /&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">XAxis</span>
            <span class="hljs-attr">dataKey</span>=<span class="hljs-string">"price"</span>
            <span class="hljs-attr">label</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">value:</span> "<span class="hljs-attr">Unit</span> <span class="hljs-attr">Price</span> ($)", <span class="hljs-attr">position:</span> "<span class="hljs-attr">insideBottom</span>", <span class="hljs-attr">offset:</span> <span class="hljs-attr">0</span>, <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span>, <span class="hljs-attr">marginTop:</span> <span class="hljs-attr">10</span> }}
            <span class="hljs-attr">tickFormatter</span>=<span class="hljs-string">{(tick)</span> =&gt;</span> `$${parseFloat(tick).toFixed(2)}`}
            tick={{ fontSize: 12 }}
            padding={{ left: 20, right: 20 }}
          /&gt;

          <span class="hljs-tag">&lt;<span class="hljs-name">YAxis</span>
            <span class="hljs-attr">label</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">value:</span> "<span class="hljs-attr">Predicted</span> <span class="hljs-attr">Sales</span> ($)", <span class="hljs-attr">angle:</span> <span class="hljs-attr">-90</span>, <span class="hljs-attr">position:</span> "<span class="hljs-attr">insideLeft</span>", <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}
            <span class="hljs-attr">tick</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">12</span> }}
            <span class="hljs-attr">tickFormatter</span>=<span class="hljs-string">{(tick)</span> =&gt;</span> `$${tick.toLocaleString()}`}
          /&gt;

          {/* tooltips with the prediction result data */}
          <span class="hljs-tag">&lt;<span class="hljs-name">Tooltip</span>
            <span class="hljs-attr">contentStyle</span>=<span class="hljs-string">{{</span>
              <span class="hljs-attr">borderRadius:</span> '<span class="hljs-attr">8px</span>',
              <span class="hljs-attr">padding:</span> '<span class="hljs-attr">10px</span>',
              <span class="hljs-attr">boxShadow:</span> '<span class="hljs-attr">0px</span> <span class="hljs-attr">0px</span> <span class="hljs-attr">15px</span> <span class="hljs-attr">rgba</span>(<span class="hljs-attr">0</span>,<span class="hljs-attr">0</span>,<span class="hljs-attr">0</span>,<span class="hljs-attr">0.5</span>)'
            }}
            <span class="hljs-attr">formatter</span>=<span class="hljs-string">{(value,</span> <span class="hljs-attr">name</span>) =&gt;</span> {
              if (name === 'sales') {
                return [`$${value.toFixed(4)}`, 'Predicted Sales']
              }
              if (name === 'volume') {
                return [`${value.toFixed(0)}`, 'Volume']
              }
              return value
            }}
            labelFormatter={(label) =&gt; `Price: $${label.toFixed(2)}`}
          /&gt;

          {/* chart area = sales */}
          <span class="hljs-tag">&lt;<span class="hljs-name">Area</span>
            <span class="hljs-attr">type</span>=<span class="hljs-string">"monotone"</span>
            <span class="hljs-attr">dataKey</span>=<span class="hljs-string">"sales"</span>
            <span class="hljs-attr">fillOpacity</span>=<span class="hljs-string">{1}</span>
            <span class="hljs-attr">fill</span>=<span class="hljs-string">"url(#colorSales)"</span>
          /&gt;</span>

          {/* vertical line for the optimal price */}
          {optimalPrice &amp;&amp;
            <span class="hljs-tag">&lt;<span class="hljs-name">ReferenceLine</span>
              <span class="hljs-attr">x</span>=<span class="hljs-string">{optimalPrice}</span>
              <span class="hljs-attr">strokeDasharray</span>=<span class="hljs-string">"4 4"</span>
              <span class="hljs-attr">ifOverflow</span>=<span class="hljs-string">"visible"</span>
              <span class="hljs-attr">label</span>=<span class="hljs-string">{{</span>
                <span class="hljs-attr">value:</span> `<span class="hljs-attr">Optimal</span> <span class="hljs-attr">Price:</span> $${<span class="hljs-attr">optimalPrice</span> !== <span class="hljs-string">null</span> &amp;&amp; <span class="hljs-attr">optimalPrice</span> &gt;</span> 0 ? Math.ceil(optimalPrice * 10000) / 10000 : ''}`,
                position: "right",
                fontSize: 12,
                offset: 10
              }}
            /&gt;
          }
        <span class="hljs-tag">&lt;/<span class="hljs-name">AreaChart</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">ResponsiveContainer</span>&gt;</span>

      {optimalPrice &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Optimal Price: $ {Math.ceil(optimalPrice * 10000) / 10000}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>}

    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App
</code></pre>
<h2 id="heading-final-results">Final Results</h2>
<p>Now, the application is ready to serve.</p>
<p>You can explore the UI from <a target="_blank" href="https://kuriko-iwai.vercel.app/online-commerce-intelligence-hub">here</a>.</p>
<p>All code (backend) is available in <a target="_blank" href="https://github.com/krik8235/ml-sales-prediction">my Github Repo</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building a machine learning system requires thoughtful project scoping and architecture design.</p>
<p>In this article, we built a dynamic pricing system as a simple single interface on containerized serverless architecture.</p>
<p>Moving forward, we’d need to consider potential drawbacks of this minimal architecture:</p>
<ul>
<li><p><strong>Increase in cold start duration</strong>: The WSGI adapter <code>awsgi</code> layer adds a small overhead. Loading a larger container image takes longer time.</p>
</li>
<li><p><strong>Monolithic function:</strong> Adding endpoints to the Lambda function can lead to a monolithic function where an issue in one endpoint impacts others.</p>
</li>
<li><p><strong>Less granular observability</strong>: AWS CloudWatch cannot provide individual invocation/error metrics per API endpoint without custom instrumentation.</p>
</li>
</ul>
<p>To scale the application effectively, extracting functionalities into a new microservice can be a good strategy to the next step.</p>
<p>I’m Kuriko IWAI, and you can find more of my work and learn more about me here:</p>
<p><a target="_blank" href="https://kuriko-iwai.vercel.app/"><strong>Portfolio</strong></a> <strong>/</strong> <a target="_blank" href="https://www.linkedin.com/in/k-i-i/"><strong>LinkedIn</strong></a> <strong>/</strong> <a target="_blank" href="https://github.com/krik8235"><strong>Github</strong></a></p>
<p><em>All images, unless otherwise noted, are by the author. This application utilizes synthetic dataset licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0) license.</em></p>
<p><em>This information about AWS is current as of August 2025 and is subject to change.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Serverless Architecture Handbook: How to Publish a Node Js Docker Image to AWS ECR and Deploy the Container to AWS Lambda ]]>
                </title>
                <description>
                    <![CDATA[ Imagine you’re tasked with building a web application that can handle incoming traffic surges as your users grow without accumulating too much cost. Sounds like a dream, right? But here’s the thing: traditionally, to do this, you would have to manage... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/serverless-architecture-with-aws-lambda/</link>
                <guid isPermaLink="false">68006521c1f51bf42a74f4b0</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ aws lambda ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ecr ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Prince Onukwili ]]>
                </dc:creator>
                <pubDate>Thu, 17 Apr 2025 02:19:13 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744843935296/c359998f-1657-482f-adf4-5ab023cb1c02.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine you’re tasked with building a web application that can handle incoming traffic surges as your users grow without accumulating too much cost. Sounds like a dream, right?</p>
<p>But here’s the thing: traditionally, to do this, you would have to manage lots of infrastructure – resources on which your application will be deployed – which can be a real headache. You’d have servers (VM instances or physical computers) to configure, databases to scale, load balancers to monitor...it’s a whole lot 😩</p>
<p>This is where Serverless architecture comes to the rescue. With the Serverless model, you can deploy your applications to handle thousands of users without you having to worry about incurring too much cost, managing infrastructure, servers, networking, and so on.</p>
<p>In this article, you’ll learn about Serverless Architecture: what it’s all about, and how to deploy your very own application using AWS Lambda. We’ll walk through the entire process step-by-step:</p>
<ul>
<li><p>How to clone your application repository using Git.</p>
</li>
<li><p>How to build an image of your application using Docker.</p>
</li>
<li><p>How to install the AWS CLI on your local machine and create AWS IAM users with the right permissions to push your Docker image to AWS Elastic Container Registry (ECR).</p>
</li>
</ul>
<p>Once the image is up and running on ECR, we’ll then connect it to AWS Lambda and deploy the container to Lambda for a fully serverless experience. 💡✨</p>
<p>Ready to go serverless? Let’s get started! 🚀</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-serverless-architecture">What is Serverless Architecture?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-differences-between-serverless-and-other-deployment-models">Differences Between Serverless and Other Deployment Models ⚡</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites-what-you-should-know-before-following-along">🧠 Prerequisites — What You Should Know Before Following Along!</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-the-application-using-git">How to Set Up the Application Using Git 🐙</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-codebase">Understanding the Codebase 🔎</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-docker-image-of-the-application">How to Create a Docker Image of the Application 🐋</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-container-registry-on-aws-elastic-container-registry-ecr">How to Create a Container Registry on AWS Elastic Container Registry (ECR) 📁</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-iam-with-aws-how-to-create-a-user-on-aws-iam-to-allow-access-to-your-aws-ecr">IAM with AWS: How to Create a User on AWS IAM to Allow Access to Your AWS ECR 👤🔐</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-upload-your-docker-image-to-the-aws-ecr-repository">How to Upload Your Docker Image to the AWS ECR repository ⬆️</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-deploy-the-application-container-to-aws-lambda-from-the-image-on-aws-ecr">How to Deploy the Application Container to AWS Lambda from the Image on AWS ECR 🚀</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-advantages-of-adopting-the-serverless-model-in-businesses">Advantages of Adopting the Serverless Model in Businesses 💼</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-disadvantages-of-the-serverless-model">Disadvantages of the Serverless Model 🚫</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-to-adopt-the-serverless-model">When to Adopt the Serverless Model 🤔</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion 📝</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-about-the-author">About the Author 👨‍💻</a></p>
</li>
</ol>
<h2 id="heading-what-is-serverless-architecture">What is Serverless Architecture?</h2>
<p>Before we dive deeper, let’s break down what we mean by Servers. In the tech world, servers are powerful computers that store, process, and manage data. Think of them as the behind-the-scenes workhorses that:</p>
<ul>
<li><p><strong>Store your data:</strong> Like a central filing cabinet for your digital documents.</p>
</li>
<li><p><strong>Run your applications:</strong> They execute the code that keeps your app or website running.</p>
</li>
<li><p><strong>Handle requests:</strong> Servers respond to user requests – like loading a webpage or processing a login.</p>
</li>
</ul>
<p>Alright, now let’s talk about Serverless Architecture – but first, let’s clear up a common misconception. When most people hear the word "Serverless", they immediately think, "Wait… no servers? How does that even work?!" 😅</p>
<p>Here’s the truth: Serverless doesn’t mean there are no servers involved (surprise, surprise! 😉). Instead, it means you, as a developer, don’t have to worry about managing the servers that your application runs on. The server-side infrastructure is fully handled by the cloud provider – in this case, AWS Lambda. You just focus on writing code and deploying it, and AWS takes care of the rest.</p>
<h3 id="heading-so-whats-the-big-deal-with-serverless">So, What’s the Big Deal with Serverless?</h3>
<p>In a traditional setup, when you deploy your application, you’re responsible for things like:</p>
<ul>
<li><p><strong>Provisioning servers</strong> (how many servers do you need? What size?)</p>
</li>
<li><p><strong>Scaling resources</strong> (how do you handle traffic spikes without overpaying?)</p>
</li>
<li><p><strong>Monitoring</strong> and keeping everything running smoothly.</p>
</li>
</ul>
<p>Sounds like a lot, right? 🤯 Well, Serverless Architecture simplifies all of that by letting you focus purely on your application code. With Lambda, you can run code in response to events (like an HTTP request, a file upload, or a database change) without worrying about the infrastructure behind it. AWS automatically scales the compute resources as needed, charging you only for the time your code is actually running. ⏱️💸</p>
<p>Imagine you’re at a restaurant. Instead of running the kitchen yourself (like managing your own servers), you just place an order (your code) and the chef (AWS Lambda) makes it for you, on-demand, based on what you need. 🍽️🍴</p>
<h2 id="heading-differences-between-serverless-and-other-deployment-models">Differences Between Serverless and Other Deployment Models ⚡</h2>
<p>Now that you understand how Serverless works, let’s take a little detour and explore the other models used to deploy applications. After all, Serverless isn’t the only kid on the block, and this will give you some important perspective when choosing the right model for your use case. 👀</p>
<p>When you build an app, you need somewhere to host it – a home for your code to live and run. Over the years, the tech world has come up with different ways to handle this, and each one gives you a different level of control (and responsibility) over your servers.</p>
<p>Let’s break it down.</p>
<h3 id="heading-infrastructure-as-a-service-iaas">🏠 Infrastructure as a Service (IaaS)</h3>
<p>With IaaS, cloud providers like AWS, Google Cloud, or Microsoft Azure give you the building blocks – virtual servers (also called instances), storage, and networking tools – but it’s still your job to set everything up.</p>
<p>It’s like renting an empty apartment. You get the walls, the doors, and the roof, but you still have to bring your own furniture, set up your Wi-Fi, and clean the place regularly. 🏡🧹</p>
<p>When you choose IaaS, you’re responsible for:</p>
<ul>
<li><p>Configuring the servers (choosing the size, the operating system, and installing software).</p>
</li>
<li><p>Handling updates, patches, and security.</p>
</li>
<li><p>Scaling up or down when traffic changes.</p>
</li>
</ul>
<p><strong>Example:</strong> Amazon EC2 (Elastic Compute Cloud) is a classic IaaS service. You rent a virtual machine, set it up yourself, and manage it like a digital landlord.</p>
<h3 id="heading-platform-as-a-service-paas">🎯 Platform as a Service (PaaS)</h3>
<p>Next up, we’ve got PaaS – a more polished setup.</p>
<p>In this model, the cloud provider takes care of the infrastructure and the underlying operating system, so you don’t have to. You just upload your code, configure a few settings, and the platform runs your app.</p>
<p>It’s like moving into a fully furnished apartment — the kitchen works, the lights are on, and the Wi-Fi is already connected. You just show up with your bags and get to work! 🧳✨</p>
<p><strong>Example:</strong> AWS Elastic Beanstalk, Heroku, or Google App Engine.</p>
<h3 id="heading-serverless-the-special-paas">🌩️ Serverless: The Special PaaS</h3>
<p>Now here’s where things get interesting: Serverless actually falls under the PaaS umbrella, but it deserves its own spotlight. Why? Because it takes the convenience of PaaS and pushes it to the next level.</p>
<p>In a traditional PaaS model (like AWS Fargate or Heroku), your application is running 24/7, whether you have visitors using it or not. You pay for the reserved space and compute power all month long, just like renting an apartment. Even if you didn’t sleep there the entire month, the bill still comes at the end. 💸🏡</p>
<p>But with Serverless, the rules change. You only pay when your code is actually being used.</p>
<h4 id="heading-how-applications-run-in-the-serverless-model">How Applications Run in the Serverless Model ⚙️</h4>
<p>In a Serverless model, your application isn’t just sitting there running all day. It “wakes up” only when it’s needed. But what exactly causes it to wake up? That’s where triggers come in.</p>
<p>Triggers are events that tell your Serverless application, “Hey, it’s time to do something!” These events could be all sorts of things, like:</p>
<ul>
<li><p>A user visiting your website and clicking a button.</p>
</li>
<li><p>Someone uploading a file to your cloud storage (like an image or document).</p>
</li>
<li><p>A new row being added to a database.</p>
</li>
<li><p>An automated schedule (like a reminder that runs every day at 8 AM).</p>
</li>
</ul>
<p>When one of these events happens, your application instantly comes to life, runs the exact task you programmed, and then goes back to “sleep” until the next trigger. This is how Serverless keeps your cloud costs low and your resources efficient – no constant running in the background, only action when there’s actually something to do!.⚡😎</p>
<p>For example, if a user sends a request that triggers your application to run for just 10 seconds and uses 20MB of memory, that’s all you pay for — the exact time and resources consumed.</p>
<p>No users? No requests? No payment. Now that’s a smart way to save money. 🧠💰</p>
<h3 id="heading-quick-comparison-paas-vs-serverless">💡 Quick Comparison: PaaS vs Serverless</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Traditional PaaS (example: AWS Fargate)</strong></td><td><strong>Serverless PaaS (example: AWS Lambda)</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Server Configuration</td><td>You select compute size &amp; limits.</td><td>No need — AWS handles it all.</td></tr>
<tr>
<td>Scaling</td><td>You configure scaling policies.</td><td>Automatic, event-driven scaling (based on incoming traffic). The higher the traffic, the more compute power is added to your application, and vice versa. 😃</td></tr>
<tr>
<td>Billing</td><td>Charged for running instances 24/7, even when idle.</td><td>Charged only when your code runs. ⏱️💸</td></tr>
<tr>
<td>Deployment</td><td>Deploy full applications.</td><td>Deploy small chunks of code (functions). You can also deploy microservices and full-scale web applications</td></tr>
</tbody>
</table>
</div><hr>
<h2 id="heading-prerequisites-what-you-should-know-before-following-along">🧠 Prerequisites — What You Should Know Before Following Along</h2>
<p>Before we dive in, here’s the best part: I wrote this article to be super beginner-friendly and detailed, so even if you have little to no programming background, you’ll still be able to follow along.</p>
<p>Whether you’re a developer, a tech-curious startup, or a business leader trying to understand modern cloud solutions, this guide was written for you.</p>
<p>That said, having some light knowledge in these areas will make the ride even smoother:</p>
<ul>
<li><p>🧑‍💻 Basic Programming Concepts – like how Node.js apps run and what a server does.</p>
</li>
<li><p>💡 Familiarity with Common Tech Terms – words like “deploy,” “application,” “CPU,” and “software” will pop up, but don’t worry: I’ve done my best to break these down into simple, relatable explanations.</p>
</li>
</ul>
<p>No prior cloud experience? No problem! This guide holds your hand all the way from setup to deployment – all in plain language, no jargon.</p>
<p>So buckle up, and let’s proceed with deploying your very own application to AWS Lambda. 😁</p>
<h2 id="heading-how-to-set-up-the-application-using-git">How to Set Up the Application Using Git 🐙</h2>
<p>Before we jump into writing code or deploying anything, the very first step is to grab the application we’ll be working with — and for that, we’ll be using Git.</p>
<p>But wait... what’s Git? — It’s a Version Control System (VCS) that helps developers track changes to their code, collaborate with teammates without stepping on each other’s toes, and safely store their work in a central place — like GitHub.</p>
<h3 id="heading-clone-the-application-repository">Clone the Application Repository 🧑‍💻</h3>
<p>I’ve already created a simple project for us to use in this tutorial — it’s sitting pretty on GitHub, waiting for you.</p>
<p>To clone the project onto your local machine, open up your terminal and run:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/onukwilip/lambda-tutorial.git
</code></pre>
<p>This command will download all the code from the <code>lambda-tutorial</code> repository into a folder on your computer. 📁</p>
<p>Once the cloning is done, navigate into the project directory like this:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> lambda-tutorial
</code></pre>
<p>Boom — just like that, your local machine is now set up with the same code that’s stored in the GitHub repo. 🏡</p>
<h2 id="heading-understanding-the-codebase">Understanding the Codebase 🔎</h2>
<h3 id="heading-open-the-codebase-in-your-favorite-ide">Open the Codebase in Your Favorite IDE 🧑‍💻</h3>
<p>For this tutorial, we’ll be using Visual Studio Code (VS Code), but feel free to use any editor you’re comfortable with.</p>
<p>Once you open the <code>lambda-tutorial</code> project folder, you’ll notice it’s a simple Node.js web server. Nothing too fancy — just a server that can handle requests and respond with some data.</p>
<p>Now, it’s important to understand what’s going on inside our codebase, especially if you’re coming from deploying on platforms like Render, Vercel, or Google Cloud Run.</p>
<h3 id="heading-deploying-to-lambda-vs-other-serverless-platforms"><strong>Deploying to Lambda vs Other Serverless Platforms ⚡</strong></h3>
<p>When you deploy to platforms like Vercel, Render, or Google Cloud Run, you usually package your web server just the way you wrote it – whether it’s a Node.js Express server or a Next.js app – and the platform handles it pretty much as-is.</p>
<p>Those platforms run your server like a mini container (or microservice) that’s always ready to handle incoming traffic, just like a waiter standing by at your table, waiting for your order.</p>
<p>But AWS Lambda works a little differently.</p>
<p>Lambda expects your code to be organized around functions – not full web servers. Think of Lambda as a chef that only shows up the moment an order is placed, cooks the food, and disappears once the job is done. 👨‍🍳🍽️</p>
<p>So if you’ve got a full-blown Node.js Express server, you’ll need to do a tiny bit of “translation” to fit Lambda’s expectations – and that’s where the lambda.js file comes in.</p>
<h4 id="heading-the-lambdajs-file-your-lambda-translator">The <code>lambda.js</code> File — Your Lambda Translator 🔀</h4>
<p>Here’s what the file looks like:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> serverless = <span class="hljs-built_in">require</span>(<span class="hljs-string">"serverless-http"</span>);
<span class="hljs-keyword">const</span> app = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./app"</span>);

<span class="hljs-keyword">const</span> handler = serverless(app);
<span class="hljs-built_in">module</span>.exports.handler = handler;
</code></pre>
<p>Let’s break it down:</p>
<ul>
<li><p><code>const serverless = require("serverless-http");</code>: This imports a handy little library called serverless-http. (The <code>serverless-http</code> library is important for our platform to run properly on AWS Lambda.) It acts like a translator: it takes your regular Express app and wraps it so that AWS Lambda can understand it.</p>
</li>
<li><p><code>const handler = serverless(app);</code>: Here’s the magic. This wraps your Express app into a Lambda-compatible function.</p>
</li>
<li><p><code>module.exports.handler = handler;</code>: This exports your wrapped function so AWS Lambda can call it when the application is triggered.</p>
</li>
</ul>
<p>So, instead of starting your server like this:</p>
<pre><code class="lang-javascript">app.listen(<span class="hljs-number">5000</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Server running on port 5000"</span>);
});
</code></pre>
<p>You’re handing your app over to Lambda and letting it handle incoming requests, scale, and run the app only when it’s needed.</p>
<h4 id="heading-the-appjs-file-your-classic-express-app">The <code>app.js</code> File — Your Classic Express App 💻</h4>
<p>Your <code>app.js</code> is where the main application logic lives. Here is usually where you:</p>
<ul>
<li><p>Set up Express.</p>
</li>
<li><p>Define routes (like <code>/api</code>, <code>/users</code>, <code>/hello</code>).</p>
</li>
<li><p>Apply middleware (like JSON parsing, logging, CORS, and so on).</p>
</li>
<li><p>Handle HTTP requests and send back responses.</p>
</li>
</ul>
<p>In a normal deployment (Render, Google Cloud Run, DigitalOcean, or your own server), you’d start the server using <code>app.listen(PORT)</code> at the bottom of this file.</p>
<p>But since we’re deploying to Lambda, you don’t directly start the server here. Instead, you export the <code>app</code> like this:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = app;
</code></pre>
<p>This way, your application stays “server-agnostic” – it’s not hardcoded to run on a traditional server. Lambda (via the <code>lambda.js</code> file) takes care of starting and stopping your app whenever it’s triggered by an event (like an HTTP request). Smart, right? 💡</p>
<p>Why this setup? 🤔</p>
<p>This little separation gives you flexibility:</p>
<ul>
<li><p>You can write your Node.js app like you always would (using <code>Express</code>) inside <code>app.js</code>.</p>
</li>
<li><p>And you only tweak the entry point (via <code>lambda.js</code>) to fit AWS Lambda’s expectations.</p>
</li>
</ul>
<h2 id="heading-how-to-create-a-docker-image-of-the-application">How to Create a Docker Image of the Application 🐋</h2>
<p>Now that we’ve had a good look at the code, let’s package it up the smart way — using Docker.</p>
<h3 id="heading-what-is-docker">What is Docker? 🐳</h3>
<p>Now, you might be wondering, <em>"Why are we using Docker?"</em></p>
<p>Docker is a software for creating images of your applications and running those images as containers. Just like real-world shipping containers hold goods securely, Docker containers hold your app, bundled with everything it needs to run: its code, libraries, dependencies, and settings. Everything is all wrapped up neatly, so your app runs the same way everywhere, whether on your laptop, AWS Lambda, or even your friend’s machine.</p>
<h3 id="heading-lets-take-a-look-at-the-dockerfile">Let’s Take a Look at the Dockerfile 🔍</h3>
<p>Inside your project folder, you’ll find a file named <code>Dockerfile</code>. This is basically the recipe that Docker uses to build your app’s container image.</p>
<p>Here’s what it looks like:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span>-slim AS builder

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> package.json .</span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm i -f</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">USER</span> root

<span class="hljs-keyword">FROM</span> amazon/aws-lambda-nodejs

<span class="hljs-keyword">ENV</span> PORT=<span class="hljs-number">5000</span>

<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/ <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/node_modules <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span>/node_modules</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/package.json <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/package-lock.json <span class="hljs-variable">${LAMBDA_TASK_ROOT}</span></span>

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">5000</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"lambda.handler"</span> ]</span>
</code></pre>
<p>Let’s break down the important steps— in plain English: 😎</p>
<ul>
<li><p><code>FROM node:18-slim AS builder</code>: We start by using a lightweight version of Node.js called <code>node:18-slim</code> and give it a tag named <code>builder</code> (think of it as Stage 1). This gives us the tools we need to build a Node.js app, but without extra stuff that makes the image heavy. The tag <code>builder</code> enables us to re-use the content of this build in the next stage</p>
</li>
<li><p><code>WORKDIR /app</code>: We set the working directory inside the container to <code>/app</code>. Think of this as telling Docker: <em>"Hey, this is the folder where I’ll be working from!"</em></p>
</li>
<li><p><code>COPY package.json .</code>: This copies the <code>package.json</code> file (which lists your app’s dependencies) into the <code>/app</code> folder inside the container.</p>
</li>
<li><p><code>RUN npm i -f</code>: This installs all the Node.js dependencies (the packages your app needs to work).<br>  The <code>-f</code> flag forces npm to resolve conflicts if any pop up.</p>
</li>
<li><p><code>COPY . .</code>: This copies the rest of your project files from your computer into the container.</p>
</li>
<li><p><code>USER root</code>: This sets the user to root (administrator level) inside the container. Useful when extra permissions are needed for certain tasks.</p>
</li>
<li><p><code>FROM amazon/aws-lambda-nodejs</code>: Now here’s the switch: we swap to the official AWS Lambda base image for Node.js! That is, Stage 2. This image is designed to work smoothly when deploying containers to Lambda.</p>
</li>
<li><p><code>ENV PORT=5000</code>: We set an environment variable for the server port. Our app will listen on port 5000.</p>
</li>
<li><p><code>COPY --from=builder /app/ ${LAMBDA_TASK_ROOT}</code>: This grabs all the files from the builder stage and copies them into Lambda’s special working directory (<code>${LAMBDA_TASK_ROOT}</code>).</p>
</li>
<li><p><code>COPY --from=builder /app/node_modules ${LAMBDA_TASK_ROOT}/node_modules</code>: Same thing, but this one specifically copies the node_modules folder (all your installed dependencies) into Lambda’s working directory.</p>
</li>
<li><p><code>COPY --from=builder /app/package.json ${LAMBDA_TASK_ROOT}</code>: Copies the <code>package.json</code> file into Lambda’s working directory.</p>
</li>
<li><p><code>COPY --from=builder /app/package-lock.json ${LAMBDA_TASK_ROOT}</code>: Copies the lock file for your dependencies – so Lambda knows exactly which versions of libraries to use.</p>
</li>
<li><p><code>EXPOSE 5000</code>: This tells Docker, <em>“Hey, my app is going to listen for requests on port 5000!"</em> (Though Lambda doesn’t use this directly, it’s useful for local testing.)</p>
</li>
<li><p><code>CMD [ "lambda.handler" ]</code>: This tells AWS Lambda which function to run when the container starts.<br>  In this case, it’s looking for a <code>handler</code> function inside your app – that’s the entry point!</p>
</li>
</ul>
<h3 id="heading-how-to-create-our-own-docker-image">How to Create Our Own Docker Image</h3>
<p>Before we proceed, you need to have Docker running on your machine. If you haven’t installed Docker yet, check out the official installation guide here: <a target="_blank" href="https://docs.docker.com/engine/install/">Docker Installation Tutorial</a>. It’s a great resource to get Docker up and running.</p>
<h4 id="heading-ensure-docker-is-running">Ensure Docker is Running</h4>
<p>Make sure Docker Desktop is installed and running. You can usually tell by the Docker icon in your system tray. If it’s not running, start it up before proceeding.</p>
<h4 id="heading-build-the-docker-image">Build the Docker Image</h4>
<p>Now, it’s time to create a Docker image of our application. In your terminal, navigate to the root directory of your project (where your Dockerfile is located). Then run the following command:</p>
<pre><code class="lang-bash">docker build -t demo-lambda-project:latest .
</code></pre>
<ul>
<li><p>The <code>docker build</code> command tells Docker to create an image.</p>
</li>
<li><p>The <code>-t demo-lambda-project:latest</code> flag assigns a tag (or name) to your image (we’ll change this later to the image naming convention supported by AWS Elastic Container Registry – ECR).</p>
<ul>
<li>Here, <code>demo-lambda-project</code> is the name, and <code>latest</code> is the tag indicating the most recent build.</li>
</ul>
</li>
<li><p>The <code>.</code> at the end tells Docker to look for the Dockerfile in the current directory.</p>
</li>
</ul>
<h4 id="heading-what-this-does">What This Does</h4>
<p>Docker will now follow the instructions in your Dockerfile step-by-step. It starts by building your Node.js app (using the lightweight Node 18 image), installs the dependencies, and then copies everything over to an AWS Lambda-ready image. Once done, you have a neat image tagged as <code>demo-lambda-project:latest</code> that’s ready for deployment.</p>
<h2 id="heading-how-to-create-a-container-registry-on-aws-elastic-container-registry-ecr">How to Create a Container Registry on AWS Elastic Container Registry (ECR) 📁</h2>
<p>Okay, let’s dive into creating an image registry on AWS Elastic Container Registry (ECR). Follow these steps closely to set up your repository named lambda-practice:</p>
<h3 id="heading-step-1-sign-in-and-navigate-to-aws-ecr">Step 1: Sign In and Navigate to AWS ECR</h3>
<p>Log in to your AWS Management Console: <a target="_blank" href="https://console.aws.amazon.com/console/home">https://console.aws.amazon.com/console/home</a>.</p>
<p>In the search bar at the top, type "ECR". You should see Amazon ECR pop up in the dropdown results. Click on it to navigate to the Elastic Container Registry section.</p>
<h3 id="heading-step-2-start-creating-your-repository">Step 2: Start Creating Your Repository</h3>
<p>Once you’re in the ECR section, look for a button that says "Create repository". Click this button to start setting up your new container registry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744649904087/615bbd21-c6ed-4243-9a18-10042eec9634.png" alt="Create new AWS ECR repository" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-3-configuring-the-repository-details">Step 3: Configuring the Repository Details</h3>
<p>You’ll need to add some info like:</p>
<ul>
<li><p><strong>Repository name:</strong> In the form that appears, enter <code>lambda-practice</code> as the repository name. This name will be used to reference your repository later when uploading your Docker image.</p>
</li>
<li><p><strong>Tag mutability:</strong> You’ll also see an option for Tag Mutability. For this tutorial, set it to Mutable. This means that if you need to update or change a tag on your image later, you can do so. (Keep in mind that in some scenarios, you might want immutable tags for images used in production environments – but mutable tags are great for testing and development, especially since we want to use the tag <code>latest</code> for our images.)</p>
</li>
</ul>
<p>When you’re happy with the settings, click the "Create repository" button at the bottom of the form.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744650070919/3010590f-f2e3-4d52-9631-8c5d4e1a5239.png" alt="Configure AWS ECR repository" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-repository-created-now-lets-take-a-look">Repository Created – Now Let's Take a Look</h3>
<p>After creating the repository, AWS will redirect you to the page listing your repositories.</p>
<p>Find the repository named <code>lambda-practice</code> in the list. This is your newly created container registry where you can push Docker images.</p>
<p>Copy the <code>lambda-practice</code> repository URI, which we’ll need later when we push our image from our local machine. The URI should be in a format similar to this - <code>&lt;aws_account_id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com/lambda-practice</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744650192129/67d724c7-15da-4ff1-8e38-638c3a8d1aa4.png" alt="Completed creation of AWS ECR repository" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And that’s it! You’ve now successfully created a container registry on AWS ECR and have your repository (<code>lambda-practice</code>) ready to receive your Docker image. 🚀</p>
<h2 id="heading-iam-with-aws-how-to-create-a-user-on-aws-iam-to-allow-access-to-your-aws-ecr">IAM with AWS: How to Create a User on AWS IAM to Allow Access to Your AWS ECR 👤🔐</h2>
<p>Now that we’ve successfully created our AWS ECR container registry (the home for our Docker image), it's time to make sure our local machine has the necessary permissions to interact with that registry. Without proper authorization, we won’t be able to upload our image.</p>
<p>To do that, we’ll create an IAM user with the appropriate permissions.</p>
<h3 id="heading-step-1-access-the-iam-console">Step 1: Access the IAM Console</h3>
<p>Start by logging in to your AWS Management Console: <a target="_blank" href="https://console.aws.amazon.com/console/home">https://console.aws.amazon.com/console/home</a>.</p>
<p>In the search bar at the top, type "IAM" and select the IAM service from the dropdown. This brings you to the IAM dashboard where you can manage users, roles, policies, and more.</p>
<h3 id="heading-step-2-navigate-to-the-users-section">Step 2: Navigate to the Users Section</h3>
<p>On the left sidebar of the IAM dashboard, click on "Users". Here you'll see a list of existing users, and this is where you'll add a new one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744651384601/085a25ca-82eb-447b-8106-46df32264a85.png" alt="Create AWS IAM User" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-3-create-a-new-user">Step 3: Create a New User</h3>
<p>Click the "Add users" button at the top. In the "Set user details" step, enter the username as <code>lambda-practice</code>.</p>
<h3 id="heading-step-4-attach-permissions-directly">Step 4: Attach Permissions Directly</h3>
<p>In the "Set permissions" step, choose "Attach policies directly". In the search box, type <code>AmazonEC2ContainerRegistryPowerUser</code>. Select the <code>AmazonEC2ContainerRegistryPowerUser</code> policy by ticking its checkbox. This policy grants the necessary permissions to work with AWS ECR, such as pushing and pulling Docker images.</p>
<p>Click Next, and verify that the username is <code>lambda-practice</code> and that the AmazonEC2ContainerRegistryPowerUser policy is attached. If everything looks good, click "Create user".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744651476901/c6d91c8c-9757-4cc6-a00f-c23d3a72de59.png" alt="Add policy to AWS IAM User" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-5-generate-access-keys-for-the-user">Step 5: Generate Access Keys for the User</h3>
<p>Once the user is created, you’ll be redirected to the page listing all IAM users. Locate and click on the user <code>lambda-practice</code>. This action will take you to the user’s summary page.</p>
<ul>
<li><p>Navigate to the "Security credentials" tab.</p>
</li>
<li><p>Under "Access keys", click the "Create access key" button.</p>
</li>
<li><p>A page will appear for configuring the new access key.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744652284582/f6a586e9-d09e-467f-ad12-81ccf538bc34.png" alt="Create Access key for AWS IAM User" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In the "Access key best practices &amp; alternatives" step, select "Command Line Interface (CLI)".</p>
<p><strong>Why should you select this option?</strong> Choosing CLI ensures that the generated access key is optimized for use with the AWS CLI and other command-line tools (like Docker commands that push images to ECR), which is exactly what we need for our workflow.</p>
<p>Leave the other configurations as their default settings, and then click "Create access key".</p>
<p>Once the key is created, you’ll see the new Access key ID and Secret access key. Make sure to copy and store these credentials securely. They are essential for authorizing your local machine to access AWS ECR and perform operations with the permissions assigned to the <code>lambda-practice</code> user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744652339772/c3d94e2a-f823-4d73-9a46-ab4d829289e9.png" alt="Completed creation of Access key for AWS IAM User" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-authorize-your-local-pc-to-publish-images-to-the-aws-ecr-repository"><strong>How to Authorize Your Local PC to Publish Images to the AWS ECR Repository</strong></h3>
<p>Now that we have our IAM user set up and the access keys in hand, it’s time to authenticate our local PC so we can securely push our Docker images to AWS ECR using the AWS CLI. Follow these steps:</p>
<h4 id="heading-step-1-install-the-aws-cli">Step 1: Install the AWS CLI</h4>
<p>If you haven’t installed the AWS CLI on your machine yet, download and install it using the official guide here: <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">Install the AWS CLI</a>.</p>
<p>This tool allows you to interact with your AWS account right from the command line, which is essential for pushing images to ECR.</p>
<h4 id="heading-step-2-configure-your-aws-cli-credentials">Step 2: Configure Your AWS CLI Credentials</h4>
<p>Once installed, you need to configure your AWS CLI to use the credentials associated with the <code>lambda-practice</code> user. Open your terminal and run the following command to set up a new profile named <code>lambda</code>:</p>
<pre><code class="lang-bash">aws configure --profile lambda
</code></pre>
<p>You’ll be prompted to enter the following details:</p>
<ul>
<li><p><strong>AWS Access Key ID:</strong> Paste the access key ID that you generated for the <code>lambda-practice</code> user.</p>
</li>
<li><p><strong>AWS Secret Access Key:</strong> Paste the corresponding secret access key.</p>
</li>
<li><p><strong>Default region name:</strong> Enter your preferred AWS region (for example, <code>us-east-1</code> or your relevant region).</p>
</li>
<li><p><strong>Default output format:</strong> You can leave this as <code>json</code> or choose your preferred format.</p>
</li>
</ul>
<p>This command configures a new CLI profile called <code>lambda</code> with the credentials of our IAM user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744652931837/650c93af-25f0-4d7b-a202-50d825a6b77a.png" alt="Authenticate and authorize AWS CLI with AWS IAM User Access key" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h4 id="heading-step-3-verify-the-configuration">Step 3: Verify the Configuration</h4>
<p>To ensure everything is set up correctly, run:</p>
<pre><code class="lang-bash">aws sts get-caller-identity --profile lambda
</code></pre>
<p>This command will return details about the IAM user configured for the <code>lambda</code> profile, confirming that your local PC is now authenticated correctly.</p>
<p>Now you’re all set! Your AWS CLI is configured with the <code>lambda</code> profile, meaning your local machine has the right credentials to interact with your AWS ECR repository and push Docker images using the permissions assigned to your <code>lambda-practice</code> IAM user.</p>
<h2 id="heading-how-to-upload-your-docker-image-to-the-aws-ecr-repository">How to Upload Your Docker Image to the AWS ECR repository ⬆️</h2>
<p>Uploading your Docker image to AWS ECR is the moment when your hard work gets sent off to your repository so AWS Lambda can later grab and run your container. Now that your PC is authorized to talk to ECR, let’s take a look at how to upload the image.</p>
<h3 id="heading-step-1-log-in-to-ecr-with-docker">Step 1: Log in to ECR with Docker</h3>
<p>Before you can push your image, you need to authenticate Docker to your AWS ECR account. You do this by running a command that gets an authentication token from AWS and pipes it to Docker. For example:</p>
<pre><code class="lang-bash">aws ecr get-login-password --region &lt;YOUR_REGION&gt; --profile lambda | docker login --username AWS --password-stdin &lt;YOUR_AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;YOUR_REGION&gt;.amazonaws.com
</code></pre>
<p>Let’s break it down:</p>
<ul>
<li><p><code>aws ecr get-login-password --region &lt;YOUR_REGION&gt; --profile lambda</code>: This part uses the AWS CLI to get a temporary login password for ECR. Be sure to replace <code>&lt;YOUR_REGION&gt;</code> with the region in which your ECR repository was created (for example, <code>us-east-1</code>).</p>
</li>
<li><p><code>| docker login --username AWS --password-stdin &lt;YOUR_AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;YOUR_REGION&gt;.</code><a target="_blank" href="http://amazonaws.com"><code>amazonaws.com</code></a>: The pipe (<code>|</code>) takes the password from the AWS CLI command and passes it as input to <code>docker login</code>. The login command then logs Docker into ECR using the provided username (<code>AWS</code>) and the password. Replace <code>&lt;YOUR_AWS_ACCOUNT_ID&gt;</code> with your actual AWS account ID.</p>
</li>
</ul>
<h3 id="heading-step-2-environment-considerations">Step 2: Environment Considerations</h3>
<p>This command works on shell environments like Powershell, zsh, and bash.</p>
<p><strong>Windows Users (CMD)</strong>:<br>If you’re using the classic Windows Command Prompt (CMD), the piping syntax might not work the same way. In that case, you might consider using Windows PowerShell or Git Bash. Alternatively, you can run the command in an environment like Windows Subsystem for Linux (WSL).</p>
<h4 id="heading-why-use-the-correct-region">Why Use the Correct Region?</h4>
<p>It is crucial to use the exact region where your ECR repository was created. The region is a part of your repository URI. If you use the wrong region, the login will fail because it won’t find the correct repository endpoint.</p>
<h4 id="heading-how-to-check-the-region">How to Check the Region:</h4>
<p>Log in to your AWS Console, navigate to the ECR section, and select your repository. The URI will look similar to this: <code>&lt;YOUR_AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;YOUR_REGION&gt;.amazonaws.com/lambda-practice</code>. Here, <code>&lt;YOUR_REGION&gt;</code> is the region you must use in your login command.</p>
<h3 id="heading-step-3-build-your-docker-image-with-the-correct-tag">Step 3: Build Your Docker Image with the Correct Tag</h3>
<p>Before pushing the image to ECR, you need to build it on your local machine and tag it with your repository’s name. In your terminal, navigate to your project’s root folder (where your Dockerfile is located), then run (replace <code>&lt;YOUR_AWS_ACCOUNT_ID&gt;</code> and <code>&lt;YOUR_REGION&gt;</code> placeholders with your AWS Account ID and AWS ECR repository region):</p>
<pre><code class="lang-bash">docker build -t &lt;YOUR_AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;YOUR_REGION&gt;.amazonaws.com/lambda-practice:latest
</code></pre>
<h3 id="heading-step-4-push-your-docker-image-to-aws-ecr">Step 4: Push Your Docker Image to AWS ECR</h3>
<p>Once your image is built and tagged, it’s time to push it to your remote ECR repository. Run the following command:</p>
<pre><code class="lang-bash">docker push &lt;YOUR_AWS_ACCOUNT_ID&gt;.dkr.ecr.&lt;YOUR_REGION&gt;.amazonaws.com/lambda-practice:latest
</code></pre>
<p>This command tells Docker to upload (or “push”) your image to the repository you created earlier.</p>
<ul>
<li><p>Make sure the repository URI and tag match what you used in the build command.</p>
</li>
<li><p>Remember, if you use a different region than the one in your repository URI, the push will fail because AWS won’t recognize the repository endpoint.</p>
</li>
</ul>
<h2 id="heading-how-to-deploy-the-application-container-to-aws-lambda-from-the-image-on-aws-ecr">How to Deploy the Application Container to AWS Lambda from the Image on AWS ECR 🚀</h2>
<p>You can deploy your function on AWS Lambda in several ways, each catering to different use cases. Here’s a quick rundown:</p>
<ol>
<li><p><strong>ZIP file upload:</strong> Simply compress your code and dependencies into a ZIP file, then upload it directly via the AWS Lambda console. This traditional method is great for small codebases that don’t require custom runtimes.</p>
</li>
<li><p><strong>Direct editing in the console:</strong> Write or edit your function code directly in the AWS Lambda code editor. Handy for quick tweaks, but not ideal for larger projects.</p>
</li>
<li><p><strong>Container image:</strong> Package your application as a Docker container image and deploy it. This approach is particularly useful if you have complex dependencies, need a custom runtime, or want consistent environments across development and production.</p>
</li>
</ol>
<p>In this tutorial, we’re taking the container image route because it offers flexibility, consistency, and scalability – all while letting us reuse our existing Docker configuration. Let’s walk through the steps for deploying your containerized application to AWS Lambda:</p>
<h3 id="heading-step-1-access-the-aws-lambda-console">Step 1: Access the AWS Lambda Console</h3>
<p>Log into your AWS Management Console. In the search bar at the top, type "Lambda" and select the AWS Lambda service from the dropdown results.</p>
<h3 id="heading-step-2-create-a-new-lambda-function">Step 2: Create a New Lambda Function</h3>
<p>Once on the Lambda page, click the "Create function" button. You’ll see multiple function creation options. For our purposes, select the "Container image" option. This choice tells AWS that you’ll be deploying a containerized application instead of uploading a ZIP file.</p>
<h3 id="heading-step-3-name-your-function">Step 3: Name Your Function</h3>
<p>In the function setup screen, enter <code>lambda-practice</code> as the name of your new Lambda function. This name identifies your function in AWS.</p>
<h3 id="heading-step-4-configure-the-container-image">Step 4: Configure the Container Image</h3>
<p>Under the “Container image” settings, click the "Browse images" button. A new window should appear, listing your available images from AWS Elastic Container Registry (ECR).</p>
<p>Select the repository you previously created (for instance, the one named <code>lambda-practice</code>), and pick the image tagged as <code>latest</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744655907615/df0e3576-5fe6-43a7-8da5-d2964b36a2af.png" alt="Create AWS Lambda function" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744655978526/fafd6b35-579a-4439-b15e-dd5e3dba2acf.png" alt="Connect AWS ECR image to AWS lambda Function" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744656031049/3de3bcc1-2034-4518-acb6-84adb6136752.png" alt="Select Image from AWS ECR repository" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-5-finalize-and-create">Step 5: Finalize and Create</h3>
<p>Now you’ll want to review the basic settings. In this step, you might also configure additional options such as memory allocation, timeout limits, and environment variables, depending on your application needs.</p>
<p>Once everything is set, click "Create function" to finalize the deployment.</p>
<h3 id="heading-how-to-enable-access-to-your-lambda-function">How to Enable Access to Your Lambda Function</h3>
<p>Awesome – hurray, you’ve successfully deployed your image from AWS ECR to AWS Lambda! Now the next step is to make sure your function is up and running and can be triggered properly. But you might be wondering, “How do I actually access my Lambda function to see if it’s working?” Let's break it down:</p>
<h4 id="heading-understanding-lambda-function-triggers">Understanding Lambda Function Triggers</h4>
<p>There are several ways to invoke a Lambda function, and AWS supports multiple trigger options. Here are a few:</p>
<ul>
<li><p><strong>Event Source Mapping:</strong> Automatically triggers your function in response to changes in services like DynamoDB, Kinesis, or S3.</p>
</li>
<li><p><strong>Scheduled Events:</strong> Set up cron-like scheduled invocations via Amazon CloudWatch Events.</p>
</li>
<li><p><strong>API Gateway:</strong> Create RESTful APIs that call your function.</p>
</li>
<li><p><strong>AWS SDK/CLI:</strong> Directly invoke the function using the AWS SDK or CLI commands.</p>
</li>
<li><p><strong>Function URLs:</strong> A simple way to expose your function over HTTPS, giving you a public URL that users or applications can call directly.</p>
</li>
</ul>
<p>In this tutorial, we’re going to use a Function URL to trigger our Lambda function via an HTTP event. This method allows you to invoke your function from the public internet and is perfect for testing or building public-facing APIs.</p>
<h3 id="heading-how-to-create-a-function-url-for-your-lambda-function">How to Create a Function URL for Your Lambda Function</h3>
<p>Now that you're on your Lambda function's details page, here’s how to create a Function URL step-by-step:</p>
<p>First, on your Lambda function’s page, click the "Configuration" tab at the top. Within the Configuration section, find and select the "Function URL" sub-tab. This is where you manage the public URL for your function.</p>
<p>Click on the "Create Function URL" button. This will open a new configuration screen for setting up your Function URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744656877335/835422c5-8c88-418a-b1f2-3650360069c3.png" alt="Create Function URL for AWS Lambda Function" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<ul>
<li><p><strong>Authentication type:</strong> Set the Auth type to NONE. This setting allows public, unauthenticated access to your function from the internet, which means anyone with the URL can invoke it. (This is great for testing or building public services, but be cautious with security in production environments!)</p>
</li>
<li><p><strong>Additional settings:</strong> Under the Additional Settings section, enable Configure cross-origin resource sharing (CORS). This is useful if you plan to call your function from client-side applications hosted on different domains. Think of it as opening a window for your app to communicate with other web pages or services.</p>
</li>
</ul>
<p>After configuring your settings, click the appropriate button to create or save the Function URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744656860868/cd98ce34-7fdf-4cb6-be85-a25d3718e2e6.png" alt="Configure AWS Function URL for AWS Lambda Function" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h4 id="heading-verify-your-function-url">Verify Your Function URL</h4>
<p>Once configured, you’ll see the Function URL displayed on the same page. You can now copy this URL.</p>
<p>Paste the URL into a browser or use tools like <code>curl</code> or Postman to send an HTTP request, triggering your Lambda function and verifying that it works as expected.</p>
<p>You should get a response just like this on your browser:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744656939019/fcda2621-8057-438b-8d5a-8ac8936b6322.png" alt="Deployed application on AWS Lambda" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And that’s it! You’ve successfully set up a public HTTP endpoint that triggers your AWS Lambda function. Whether you're testing your deployment or building a public-facing API, the Function URL makes it easy for anyone to interact with your function.</p>
<h3 id="heading-congrats-you-did-it"><strong>Congrats — You did it!</strong></h3>
<p>You've just walked through the entire journey of deploying a Node.js web server, containerized with Docker, all the way to AWS Lambda using AWS ECR as your image repository. 🚀</p>
<p>From writing and containerizing your Node.js application, creating an AWS ECR repository, setting up IAM users and access keys, pushing your Docker image to ECR, to deploying it on Lambda – you’ve covered it all like a pro. 💪</p>
<p>Not only that, but you also configured a public-facing Function URL so your serverless app can now handle requests from anywhere in the world 🌍.</p>
<p>You’ve just combined modern cloud-native workflows with serverless deployment – giving you flexibility, scalability, and lightning-fast response times without the headache of managing servers 😁.</p>
<p>👏 Give yourself a pat on the back. You’ve officially containerized and deployed your Node.js web server to AWS Lambda!</p>
<h2 id="heading-advantages-of-adopting-the-serverless-model-in-businesses">Advantages of Adopting the Serverless Model in Businesses 💼</h2>
<p>When it comes to deploying applications in the cloud, the serverless model has truly flipped the old playbook and has helped businesses save on Cloud costs! Let’s break it down in simple, real-world terms.</p>
<h3 id="heading-cost-efficiency"><strong>Cost-Efficiency 💰</strong></h3>
<p>For most businesses – especially startups – serverless offers a major financial advantage. Here’s why:</p>
<p>In traditional models like IaaS (Infrastructure as a Service) and PaaS (Platform as a Service), such as using AWS EC2 or AWS Elastic Beanstalk, you provision resources upfront.</p>
<p>For example: You spin up a server with 4 GB RAM and 4 vCPUs, and AWS charges you $100/month (this covers 730 hours – the whole month). Even if your app barely does anything – say it only serves real requests for 120 hours, and uses just 1 GB of memory – you still pay the full $100, because the resources were reserved and waiting for traffic 24/7.</p>
<p>But with Serverless:</p>
<ul>
<li><p>You don’t pre-allocate or reserve compute power.</p>
</li>
<li><p>Your application only runs when someone actually needs it (for example, when a user makes an HTTP request).</p>
</li>
<li><p>You only pay for the actual execution time and the resources used.</p>
</li>
</ul>
<p>For instance, if your function only runs for 50 hours in a month and uses 1.5 GB RAM, you might pay something like $30, compared to the flat $100 you'd have paid on EC2 or Elastic Beanstalk.</p>
<h3 id="heading-scalability-without-stress"><strong>Scalability Without Stress 📈</strong></h3>
<p>Serverless platforms like AWS Lambda automatically handle:</p>
<ul>
<li><p>Scaling up during high demand.</p>
</li>
<li><p>Scaling down to zero when idle.</p>
</li>
</ul>
<p>This means your team won’t need to predict or provision for resources during traffic surges. Whether 1 or 1 million users visit your app, the cloud provider handles the rest.</p>
<h3 id="heading-simplified-operations"><strong>Simplified Operations ⚙️</strong></h3>
<p>For your software team:</p>
<ul>
<li><p>No more babysitting servers, patching security updates, or worrying about load balancers.</p>
</li>
<li><p>You focus purely on writing the business logic and shipping code.</p>
</li>
<li><p>The cloud provider handles the infrastructure behind the scenes.</p>
</li>
</ul>
<p>This frees up your team’s time, cuts maintenance tasks, and speeds up development times.</p>
<h3 id="heading-better-return-on-investment-roi"><strong>Better Return on Investment (ROI) 📊</strong></h3>
<p>Because you only pay for what you use, the cost-to-value ratio improves significantly. Startups and businesses can:</p>
<ul>
<li><p>Launch faster.</p>
</li>
<li><p>Experiment without financial risk.</p>
</li>
<li><p>Scale without surprise bills.</p>
</li>
<li><p>Avoid overpaying for idle resources.</p>
</li>
</ul>
<h2 id="heading-disadvantages-of-the-serverless-model">Disadvantages of the Serverless Model 🚫</h2>
<p>As exciting and cost-friendly as the serverless model seems, the golden rule in tech still applies:<br>every solution comes with trade-offs.</p>
<p>Let’s walk through a few important downsides you should consider:</p>
<h3 id="heading-no-built-in-support-for-background-jobs"><strong>No Built-in Support for Background Jobs ⏰</strong></h3>
<p>Unlike traditional servers where you can run background processes – like sending out newsletters at midnight or cleaning up databases at scheduled times – serverless platforms such as AWS Lambda don’t natively support background tasks or recurring jobs.</p>
<p>For example, let’s say you wanted your app to automatically generate reports every day at 3 AM. In a typical server setup, you’d just write a cron job and call it a day.</p>
<p>But with Lambda or serverless, you can’t do this directly inside your deployed function. Instead, you need external tools like:</p>
<ul>
<li><p>AWS EventBridge (for scheduling and triggering Lambda functions)</p>
</li>
<li><p>Or other cloud-native schedulers.</p>
</li>
</ul>
<p>This adds a bit of extra setup, management, and sometimes extra cost.</p>
<h3 id="heading-unpredictable-cloud-costs"><strong>Unpredictable Cloud Costs 💸</strong></h3>
<p>One of the biggest selling points of serverless is “pay-as-you-use” – but this can also become a financial blind spot, because:</p>
<ul>
<li><p>Costs depend on traffic volume and resource usage.</p>
</li>
<li><p>If your app suddenly goes viral or experiences a traffic spike, your cloud bill could skyrocket without warning.</p>
</li>
</ul>
<p>For example, an app that runs stable at $30/month for low traffic could unexpectedly hit $1000+ if a marketing campaign or external event drives huge numbers of users to your service. While this means your app is succeeding, your budget might take a hit.</p>
<p>In contrast, with traditional models like AWS EC2 or Elastic Beanstalk, your costs are usually predictable – even if your server sits idle all month.</p>
<h2 id="heading-when-to-adopt-the-serverless-model">When to Adopt the Serverless Model 🤔</h2>
<p>So, is Serverless always the right choice? Not necessarily!</p>
<p>If you expect:</p>
<ul>
<li><p><strong>Steady, predictable workloads,</strong> EC2 or Elastic Beanstalk might offer more cost certainty.</p>
</li>
<li><p><strong>Long-running background tasks</strong>, serverless isn’t ideal without extra services.</p>
</li>
<li><p><strong>Real-time control over resource limits</strong>, traditional servers give you more flexibility.</p>
</li>
</ul>
<p>But if your app has burst traffic (users come and go), event-driven logic (like APIs or webhooks), or you want minimal ops overhead, then Serverless can save time, effort, and money.</p>
<h3 id="heading-when-serverless-is-the-perfect-fit-a-startup-building-an-event-driven-api"><strong>When Serverless is the Perfect Fit: A Startup Building an Event-Driven API</strong></h3>
<p>Imagine you’re running a small tech startup that just launched an app for booking fitness classes. Your team is small, budgets are tight, and traffic is unpredictable – some days you have 50 users, some days 5,000.</p>
<p>In this case:</p>
<ul>
<li><p>Your backend mostly handles HTTP requests: new sign-ups, class bookings, cancellations, and payments.</p>
</li>
<li><p>Traffic spikes during lunch breaks and weekends, but is quiet at night.</p>
</li>
<li><p>You don’t want to hire a full-time DevOps engineer just to manage servers.</p>
</li>
</ul>
<p>👉 <strong>Why Serverless is perfect in this case:</strong></p>
<ul>
<li><p>You only pay when people use your app.</p>
</li>
<li><p>No need to manage or provision servers.</p>
</li>
<li><p>AWS Lambda auto-scales based on demand.</p>
</li>
<li><p>Fast to deploy, easy to connect to other AWS services (like DynamoDB for your database, S3 for images, and SES for emails).</p>
</li>
</ul>
<p>By using Serverless in this case, you can save money, scale automatically, and stay laser-focused on features – not infrastructure.</p>
<h3 id="heading-when-serverless-is-not-a-good-fit-a-video-streaming-platform"><strong>When Serverless is Not a Good Fit: A Video Streaming Platform</strong></h3>
<p>Now imagine you’re building the next YouTube-like service for a niche audience – say, education-based content for universities.</p>
<p>In this case:</p>
<ul>
<li><p>Your platform requires continuous background processing: encoding videos, generating thumbnails, and pushing them to CDN.</p>
</li>
<li><p>Users stream content 24/7, meaning your app is always under load.</p>
</li>
<li><p>Background jobs like recommendation engine updates or nightly reports need to run frequently.</p>
</li>
</ul>
<p>👉 <strong>Why Serverless might be a bad idea:</strong></p>
<ul>
<li><p>Functions like AWS Lambda have a timeout limit (for example 15 minutes max per execution).</p>
</li>
<li><p>Continuous processing or streaming doesn’t fit the on-demand, short-lived nature of serverless.</p>
</li>
<li><p>Costs could skyrocket since the app runs almost all the time, making it more expensive than a dedicated EC2 or Kubernetes cluster.</p>
</li>
</ul>
<p><strong>Better alternative:</strong><br>For this kind of use case, a traditional server-based setup – like EC2 or container orchestration via ECS or Kubernetes – would offer more control, predictable pricing, and support for long-running processes</p>
<p>✅ <strong>Bottom line:</strong><br>Serverless is fantastic for modern apps, but like any tool, it’s best used when its strengths match your project’s needs.</p>
<h2 id="heading-conclusion">Conclusion 📝</h2>
<p>Congratulations on making it to the end of this tutorial! 🚀</p>
<p>In this article, we explored the power of serverless computing by walking step-by-step through the process of deploying a Node.js web server using Docker and AWS Lambda.</p>
<p>From building your container image, pushing it to AWS ECR, and finally deploying it on Lambda – you’ve now seen how easy it is to get an app running without the hassle of provisioning servers.</p>
<p>We also discussed the advantages of adopting the Serverless model in deploying your applications, it’s disadvantages, and real-world use cases in which you should adopt the serverless approach.</p>
<h2 id="heading-about-the-author"><strong>About the Author 👨‍💻</strong></h2>
<p>Hi, I’m Prince! I’m a DevOps engineer and Cloud architect passionate about building, deploying, and managing scalable applications and sharing knowledge with the tech community.</p>
<p>If you enjoyed this article, you can learn more about me by exploring more of my blogs and projects on my <a target="_blank" href="https://www.linkedin.com/in/prince-onukwili-a82143233/">LinkedIn profile</a>. You can find my <a target="_blank" href="https://www.linkedin.com/in/prince-onukwili-a82143233/details/publications/">LinkedIn articles here</a>. You can also <a target="_blank" href="https://prince-onuk.vercel.app/achievements#articles">visit my website</a> to read more of my articles as well. Let’s connect and grow together! 😊</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Serverless CRUD REST API with the Serverless Framework, Node.js, and GitHub Actions ]]>
                </title>
                <description>
                    <![CDATA[ Serverless computing emerged as a response to the challenges of traditional server-based architectures. With serverless, developers no longer need to manage or scale servers manually. Instead, cloud providers handle infrastructure management, allowin... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-serverless-crud-rest-api/</link>
                <guid isPermaLink="false">66c63e8f9aca8203eaa3e7e3</guid>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless framework ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ifeanyi Otuonye ]]>
                </dc:creator>
                <pubDate>Wed, 21 Aug 2024 19:22:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724267592147/e9dc4429-6475-4d35-b0e8-81c116f769b8.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Serverless computing emerged as a response to the challenges of traditional server-based architectures. With serverless, developers no longer need to manage or scale servers manually. Instead, cloud providers handle infrastructure management, allowing teams to focus solely on writing and deploying code.</p>
<p>Serverless solutions automatically scale based on demand and offer a pay-as-you-go model. This means that you only pay for the resources your application actually uses. This approach significantly reduces operational overhead, increases flexibility and accelerates development cycles, making it an attractive option for modern application development.</p>
<p>By abstracting server management, Serverless platforms let you concentrate on business logic and application functionality. This leads to faster deployments and more innovation. Serverless architectures are also event-driven, which means they can automatically respond to real-time events and scale to meet user demands without manual intervention.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-important-concepts-to-understand">Important Concepts to Understand</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-application-programming-interface-api">Application Programming Interface (API)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-http-methods">HTTP Methods</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-amazon-api-gateway">Amazon API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-amazon-dynamodb">Amazon DynamoDB</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-serverless-crud-application">Serverless CRUD Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-serverless-framework">The Serverless Framework</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-github-actions">GitHub Actions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-postman">Postman</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-our-use-case">Our Use Case</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tutorial-objectives">Tutorial Objectives</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-get-started-clone-the-git-repository">How to get Started:Clone the Git Repository</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-the-serverless-framework-environment">Step 1: Set up the Serverless Framework Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-define-the-api-in-the-serverless-yaml-file">Step 2: Define the API in the Serverless YAML File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-develop-the-lambda-functions-for-crud-operations">Step 3: Develop the Lambda Functions for CRUD Operations</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-coffee-lambda-function">Create Coffee Lambda function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-get-coffee-lambda-function">Get Coffee Lambda function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-update-coffee-lambda-function">Update Coffee Lambda function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-delete-coffee-lambda-function">Delete Coffee Lambda function</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-set-up-cicd-pipeline-multi-stage-deployments-for-dev-and-prod-environments">Step 4: Set Up CI/CD Pipeline Multi-stage Deployments for Dev and Prod Environments</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-test-the-dev-and-prod-pipelines">Step 5: Test the Dev and Prod Pipelines</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-test-and-validate-prod-and-dev-apis-using-postman">Step 6: Test and Validate Prod and Dev APIs using Postman</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<p>Before diving into the technical details, we'll go over some key background concepts.</p>
<h2 id="heading-important-concepts-to-understand">Important Concepts to Understand</h2>
<h3 id="heading-application-programming-interface-api">Application Programming Interface (API)</h3>
<p>An Application Programming Interface (API) allows different software applications to communicate and interact with each other. It defines the methods and data formats that applications can use to request and exchange information for integration and data sharing between diverse systems.</p>
<h3 id="heading-http-methods">HTTP Methods</h3>
<p>HTTP methods or request methods are a critical component of web services and APIs. They indicate the desired action to be performed on a resource in a given request URL.</p>
<p>The most commonly used methods in RESTful APIs are:</p>
<ul>
<li><p><strong>GET</strong>: used to retrieve data from a server</p>
</li>
<li><p><strong>POST</strong>: sends data, included in the body of the request, to create or update a resource</p>
</li>
<li><p><strong>PUT</strong>: updates or replaces an existing resource or creates a new resource if it doesn’t exist</p>
</li>
<li><p><strong>DELETE</strong>: deletes the specified data from the server.</p>
</li>
</ul>
<h3 id="heading-amazon-api-gateway">Amazon API Gateway</h3>
<p>Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor and secure APIs at scale. It acts as an entry point for multiple APIs, managing and controlling the interactions between clients (such as web or mobile applications) and backend services.</p>
<p>It also provides various functions, including request routing, security, authentication, caching and rate limiting that help simplify the management and deployment of APIs.</p>
<h3 id="heading-amazon-dynamodb">Amazon DynamoDB</h3>
<p>DynamoDB is a fully managed NoSQL database service designed for high scalability, low latency, and replication of data across multiple regions.</p>
<p>DynamoDB stores data in a schema-less format, allowing for flexible and fast storage and retrieval of structured and semi-structured data. It is commonly used for building scalable and responsive applications in cloud-based environments.</p>
<h3 id="heading-serverless-crud-application">Serverless CRUD Application</h3>
<p>A serverless CRUD application refers to the ability to <strong>Create, Read, Update and Delete</strong> data. But the architecture and components involved differ from traditional server-based applications.</p>
<p><strong>Create</strong> involves adding new entries to a DynamoDB table. The <strong>Read</strong> operation retrieves data from a DynamoDB table. <strong>Update</strong> updates existing data in DynamoDB. And the <strong>Delete</strong> operation deletes data from DynamoDB.</p>
<h3 id="heading-the-serverless-framework">The Serverless Framework</h3>
<p>The Serverless Framework is an open-source tool that simplifies the deployment and management of serverless applications across multiple cloud providers, including AWS. It abstracts away the complexity of provisioning and managing infrastructure by allowing developers to define their infrastructure as code using a YAML file.</p>
<p>The framework handles the deployment, scaling and updating of serverless functions, APIs and other resources.</p>
<h3 id="heading-github-actions">GitHub Actions</h3>
<p>GitHub Actions is a powerful CI/CD automation tool that allows developers to automate their software workflows directly from their GitHub repository.</p>
<p>With GitHub Actions, you can create custom pipelines triggered by events such as code pushes, pull requests, or branch merges. These workflows are defined in YAML files within the repository and can perform tasks like testing, building and deploying applications to various environments.</p>
<h3 id="heading-postman">Postman</h3>
<p>Postman is a popular collaboration platform that simplifies the process of designing, testing, and documenting APIs. It offers a user-friendly interface for developers to create and send HTTP requests, test API endpoints, and automate testing workflows.</p>
<p>Alright, now that you're familiar with the tools and technologies we'll use here, let's dive in.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js and npm installed</p>
</li>
<li><p>AWS CLI configured with access to your AWS account</p>
</li>
<li><p>A Serverlesss Framework account</p>
</li>
<li><p>Serverlesss Framework globally installed in your local CLI</p>
</li>
</ul>
<h2 id="heading-our-use-case">Our Use Case</h2>
<p>Meet Alyx, an entrepreneur who has recently been learning about serverless architecture. She's read about how it's a powerful and efficient way to build backends for web applications, offering a more modern approach to web application development.</p>
<p>She wants to apply what she's learned so far about of the fundamentals of AWS serverless  computing. She knows that serverless doesn’t mean there are no servers involved – rather, it just abstracts away the management and provisioning of servers. And now she wants to focus solely on writing code and implementing business logic.</p>
<p>Let’s check out how Alyx, the owner of a thriving coffee shop, begins to leverage serverless architecture for the backend of her web application.</p>
<p>Alyx’s Coffee Haven, an online coffee shop, offers an array of coffee blends and treats for sale. Initially, Alyx managed the shop’s orders and inventory with traditional web hosting services and operations, where she handled multiple servers and resources. But as her coffee shop grew in popularity, she started facing an increasing number of orders, especially during peak hours and seasonal promotions.</p>
<p>Managing the servers and ensuring the application could handle the surge in traffic became a challenge for Alyx. She found herself constantly worrying about server capacity, scalability, and the cost of maintaining the infrastructure.</p>
<p>She also wanted to introduce new features like personalized recommendations and loyalty programs, but this became a daunting task given the limitations of her traditional setup.</p>
<p>Then Alyx learned about the concept of serverless. She likened a serverless backend to a barista who automatically brews coffee in real-time, without her having to worry about the intricate details of the coffee-making process.</p>
<p>Excited by this idea, Alyx decided to migrate her coffee shop’s backend to a serverless platform using AWS Lambda, AWS API Gateway, and Amazon DynamoDB. This setup will let her focus more on crafting the perfect coffee blends and treats for her customers.</p>
<p>With serverless, each customer’s order becomes an event that triggers a series of serverless functions. Separate AWS Lambda functions processes the orders and handles all the business logic behind the scenes. For instance, it creates a customer’s order and is able to retrieve that order. It can also delete someone's order or update an order’s status.</p>
<p>Alyx no longer needs to worry about managing servers, as the serverless platform automatically scales up and down based on incoming order requests. Also, the cost-efficiency of serverless is huge for Alyx. With a pay-as-you-go model, she only pays for the actual compute time her functions consume, offering her a more a cost-effective solution for her growing business.</p>
<p>But she doesn’t stop there! She also wants to automate everything, from deploying infrastructure to updating her application whenever there’s a new change. By utilizing Infrastructure as Code (IaC) with the Serverless Framework, she can define all her infrastructure in code and manage it easily.</p>
<p>On top of that, she sets up GitHub Actions for continuous integration and delivery (CI/CD), so that every change she makes is automatically deployed through a pipeline, whether it’s a new feature in development or a hot fix for production.</p>
<h2 id="heading-tutorial-objectives">Tutorial Objectives</h2>
<ul>
<li><p>Set up the Serverless Framework environment</p>
</li>
<li><p>Define an API in the YAML file</p>
</li>
<li><p>Develop AWS Lambda functions to process CRUD operations</p>
</li>
<li><p>Set up multi-stage deployments for Dev and Prod</p>
</li>
<li><p>Test the Dev and Prod pipelines</p>
</li>
<li><p>Test and validate Dev and Prod APIs using Postman</p>
</li>
</ul>
<h2 id="heading-how-to-get-started-clone-the-git-repository">How to Get Started: Clone the Git Repository</h2>
<p>To enhance your understanding and so you can follow along with this tutorial more effectively, go ahead and clone the project’s repository from my GitHub. You can do that <a target="_blank" href="https://github.com/ifeanyiro9/coffee-shop-serverless-crud-api-nodejs">by going here</a>. As we move forward, feel free to edit the files as you feel necessary.</p>
<p>After cloning the repository, you will notice the presence of multiple files in your folder, as you can see in the image below. We’ll use all of these files to build our serverless coffee shop API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353622612/2dd67caa-1a30-4511-afc5-babfaa0c5b82.png" alt="File structure" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-step-1-set-up-the-serverless-framework-environment">Step 1: Set up the Serverless Framework Environment</h2>
<p>To set up the Serverless Framework environment for automated deployments, you'll need to authenticate your Serverless Framework account via the CLI.</p>
<p>This requires creating an access key that enables the CI/CD pipeline and utilizes the Serverless Framework to authenticate securely into your account without exposing your credentials. By signing into your Serverless account and generating an access key, the pipeline can deploy your serverless application automatically from the build configuration file.</p>
<p>To do this, head to your Serverless account and <a target="_blank" href="https://app.serverless.com/settings/accessKeys">navigate to the Access Keys section</a>. Click on “+add,” name it SERVERLESS_ACCESS_KEY, and then create the key.</p>
<p>Once you’ve created your access key, be sure to copy and store it securely. You'll use this key as a secret variable in your GitHub repository to authenticate and authorize your CI/CD pipeline.</p>
<p>It will provide access to your Serverless Framework account during the deployment process. You’ll add this key to your GitHub repository’s secrets later, so your pipeline can securely use it to deploy the serverless resources without exposing sensitive information in your codebase.</p>
<p>Now, let’s define the AWS resources as code in the <strong>severless.yaml</strong> file.</p>
<h2 id="heading-step-2-define-the-api-in-the-serverless-yaml-file">Step 2: Define the API in the Serverless YAML File</h2>
<p>In this file, you'll define the core infrastructure and functionality of the Coffee Shop API using the Serverless Framework’s YAML configuration.</p>
<p>This file defines the AWS services being utilized, including API Gateway, Lambda functions for CRUD operations, and DynamoDB for data storage.</p>
<p>You'll also configure an IAM role so the Lambda functions have the necessary permissions to interact with the DynamoDB service.</p>
<p>The API Gateway is set up with appropriate HTTP methods (<strong>POST</strong>, <strong>GET</strong>, <strong>PUT</strong>, and <strong>DELETE</strong>) to handle incoming requests and trigger the corresponding Lambda functions.</p>
<p>Let’s check out the code:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">coffee-shop-api</span>
<span class="hljs-attr">frameworkVersion:</span> <span class="hljs-string">'4'</span>

<span class="hljs-attr">provider:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">aws</span>
  <span class="hljs-attr">runtime:</span> <span class="hljs-string">nodejs20.x</span>
  <span class="hljs-attr">region:</span> <span class="hljs-string">us-east-1</span>
  <span class="hljs-attr">stage:</span> <span class="hljs-string">${opt:stage}</span>
  <span class="hljs-attr">iam:</span>
    <span class="hljs-attr">role:</span>
      <span class="hljs-attr">statements:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
          <span class="hljs-attr">Action:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:PutItem</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:GetItem</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:Scan</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:UpdateItem</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">dynamodb:DeleteItem</span>
          <span class="hljs-attr">Resource:</span> <span class="hljs-string">arn:aws:dynamodb:${self:provider.region}:*:table/CoffeeOrders-${self:provider.stage}</span>

<span class="hljs-attr">functions:</span>
  <span class="hljs-attr">createCoffee:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">createCoffee.handler</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">COFFEE_ORDERS_TABLE:</span> <span class="hljs-string">CoffeeOrders-${self:provider.stage}</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">coffee</span>
          <span class="hljs-attr">method:</span> <span class="hljs-string">post</span>

  <span class="hljs-attr">getCoffee:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">getCoffee.handler</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">COFFEE_ORDERS_TABLE:</span> <span class="hljs-string">CoffeeOrders-${self:provider.stage}</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">coffee</span>
          <span class="hljs-attr">method:</span> <span class="hljs-string">get</span>

  <span class="hljs-attr">updateCoffee:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">updateCoffee.handler</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">COFFEE_ORDERS_TABLE:</span> <span class="hljs-string">CoffeeOrders-${self:provider.stage}</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>  
          <span class="hljs-attr">path:</span> <span class="hljs-string">coffee</span>  
          <span class="hljs-attr">method:</span> <span class="hljs-string">put</span>  

  <span class="hljs-attr">deleteCoffee:</span>  
    <span class="hljs-attr">handler:</span> <span class="hljs-string">deleteCoffee.handler</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">COFFEE_ORDERS_TABLE:</span> <span class="hljs-string">CoffeeOrders-${self:provider.stage}</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">coffee</span>
          <span class="hljs-attr">method:</span> <span class="hljs-string">delete</span>
<span class="hljs-attr">resources:</span>
  <span class="hljs-attr">Resources:</span>
    <span class="hljs-attr">CoffeeTable:</span>
      <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::DynamoDB::Table</span>
      <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">TableName:</span> <span class="hljs-string">CoffeeOrders-${self:provider.stage}</span>
        <span class="hljs-attr">AttributeDefinitions:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">OrderId</span>
            <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">CustomerName</span>
            <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
        <span class="hljs-attr">KeySchema:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">OrderId</span>
            <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">CustomerName</span>
            <span class="hljs-attr">KeyType:</span> <span class="hljs-string">RANGE</span>
        <span class="hljs-attr">BillingMode:</span> <span class="hljs-string">PAY_PER_REQUEST</span>
</code></pre>
<p>The <strong>serverless.yml</strong> configuration defines how Alyx's Coffee Shop API will run in a serverless environment on AWS. The <strong>provider</strong> section specifies that the application will use AWS as the cloud provider, with <strong>Node.js</strong> as the runtime environment.</p>
<p>The region is set to <strong>us-east-1</strong> and the <strong>stage</strong> variable allows for dynamic deployment across different environments, like dev and prod. This means that the same code can deploy to different environments, with resources being named accordingly to avoid conflicts.</p>
<p>In the <strong>iam</strong> section, permissions are granted to Lambda functions to interact with the DynamoDB table. The <strong>${self:provider.stage}</strong> syntax dynamically names the DynamoDB table, so that each environment has its own separate resources, like <strong>CoffeeOrders-dev</strong> for the development environment and <strong>CoffeeOrders-prod</strong> for production. This dynamic naming helps manage multiple environments without manually configuring separate tables for each one.</p>
<p>The <strong>functions</strong> section defines the four core Lambda functions, <strong>createCoffee</strong>, <strong>getCoffee</strong>, <strong>updateCoffee</strong> and <strong>deleteCoffee</strong>. These handle the CRUD operations for the Coffee Shop API.</p>
<p>Each function is connected to a specific HTTP method in the API Gateway, such as <strong>POST</strong>, <strong>GET</strong>, <strong>PUT</strong> and <strong>DELETE</strong>. These functions interact with the DynamoDB table that’s dynamically named based on the current stage.</p>
<p>The last <strong>resources</strong> section defines the DynamoDB table itself. It sets up the table with the attributes <strong>OrderId</strong> and <strong>CustomerName</strong>, which are used as the primary key. The table is configured to use a pay-per-request billing mode, making it cost-effective for Alyx's growing business.</p>
<p>By automating the deployment of these resources using the Serverless Framework, Alyx can easily manage her infrastructure, freeing her from the burden of manually provisioning and scaling resources.</p>
<h2 id="heading-step-3-develop-the-lambda-functions-for-crud-operations">Step 3: Develop the Lambda Functions for CRUD Operations</h2>
<p>In this step, we implement the core logic of Alyx’s Coffee Shop API by creating Lambda functions with JavaScript that perform the essential CRUD operations <strong>createCoffee</strong>, <strong>getCoffee</strong>, <strong>updateCoffee</strong> and <strong>deleteCoffee</strong>.</p>
<p>These functions utilize the AWS SDK to interact with AWS services, particularly DynamoDB. Each function will be responsible for handling specific API requests such as creating an order, retrieving orders, updating order statuses, and deleting orders.</p>
<h3 id="heading-create-coffee-lambda-function">Create Coffee Lambda function</h3>
<p>This function creates an order:</p>
<pre><code class="lang-yaml"><span class="hljs-string">const</span> <span class="hljs-string">AWS</span> <span class="hljs-string">=</span> <span class="hljs-string">require('aws-sdk');</span>
<span class="hljs-string">const</span> <span class="hljs-string">dynamoDb</span> <span class="hljs-string">=</span> <span class="hljs-string">new</span> <span class="hljs-string">AWS.DynamoDB.DocumentClient();</span>
<span class="hljs-string">const</span> { <span class="hljs-attr">v4:</span> <span class="hljs-string">uuidv4</span> } <span class="hljs-string">=</span> <span class="hljs-string">require('uuid');</span>

<span class="hljs-string">module.exports.handler</span> <span class="hljs-string">=</span> <span class="hljs-string">async</span> <span class="hljs-string">(event)</span> <span class="hljs-string">=&gt;</span> {
  <span class="hljs-string">const</span> <span class="hljs-string">requestBody</span> <span class="hljs-string">=</span> <span class="hljs-string">JSON.parse(event.body);</span>
  <span class="hljs-string">const</span> <span class="hljs-string">customerName</span> <span class="hljs-string">=</span> <span class="hljs-string">requestBody.customer_name;</span>
  <span class="hljs-string">const</span> <span class="hljs-string">coffeeBlend</span> <span class="hljs-string">=</span> <span class="hljs-string">requestBody.coffee_blend;</span>
  <span class="hljs-string">const</span> <span class="hljs-string">orderId</span> <span class="hljs-string">=</span> <span class="hljs-string">uuidv4();</span>

  <span class="hljs-string">const</span> <span class="hljs-string">params</span> <span class="hljs-string">=</span> {
    <span class="hljs-attr">TableName:</span> <span class="hljs-string">process.env.COFFEE_ORDERS_TABLE</span>,
    <span class="hljs-attr">Item:</span> {
      <span class="hljs-attr">OrderId:</span> <span class="hljs-string">orderId</span>,
      <span class="hljs-attr">CustomerName:</span> <span class="hljs-string">customerName</span>,
      <span class="hljs-attr">CoffeeBlend:</span> <span class="hljs-string">coffeeBlend</span>,
      <span class="hljs-attr">OrderStatus:</span> <span class="hljs-string">'Pending'</span>
    }
  }<span class="hljs-string">;</span>

  <span class="hljs-string">try</span> {
    <span class="hljs-string">await</span> <span class="hljs-string">dynamoDb.put(params).promise();</span>
    <span class="hljs-string">return</span> {
      <span class="hljs-attr">statusCode:</span> <span class="hljs-number">200</span>,
      <span class="hljs-attr">body:</span> <span class="hljs-string">JSON.stringify(</span>{ <span class="hljs-attr">message:</span> <span class="hljs-string">'Order created successfully!'</span>, <span class="hljs-attr">OrderId:</span> <span class="hljs-string">orderId</span> }<span class="hljs-string">)</span>
    }<span class="hljs-string">;</span>
  } <span class="hljs-string">catch</span> <span class="hljs-string">(error)</span> {
    <span class="hljs-string">return</span> {
      <span class="hljs-attr">statusCode:</span> <span class="hljs-number">500</span>,
      <span class="hljs-attr">body:</span> <span class="hljs-string">JSON.stringify(</span>{ <span class="hljs-attr">error:</span> <span class="hljs-string">`Could</span> <span class="hljs-attr">not create order:</span> <span class="hljs-string">$</span>{<span class="hljs-string">error.message</span>}<span class="hljs-string">`</span> }<span class="hljs-string">)</span>
    }<span class="hljs-string">;</span>
  }
}<span class="hljs-string">;</span>
</code></pre>
<p>This Lambda function handles the creation of a new coffee order in the DynamoDB table. First we import the AWS SDK and initialize a <strong>DynamoDB.DocumentClient</strong> to interact with DynamoDB. The <strong>uuid</strong> library is also imported to generate unique order IDs.</p>
<p>Inside the <strong>handler</strong> function, we parse the incoming request body to extract customer information, such as the customer's name and preferred coffee blend. A unique <strong>orderId</strong> is generated using <strong>uuidv4()</strong> and this data is prepared for insertion into DynamoDB.</p>
<p>The <strong>params</strong> object defines the table where the data will be stored, with <strong>TableName</strong> dynamically set to the value of the environment variable <strong>COFFEE_ORDERS_TABLE</strong>. The new order includes fields such as <strong>OrderId</strong>, <strong>CustomerName</strong>, <strong>CoffeeBlend</strong>, and an initial status of <strong>Pending</strong>.</p>
<p>In the <strong>try</strong> block, the code attempts to add the order to the DynamoDB table using the <strong>put()</strong> method. If successful, the function returns a status code of <strong>200</strong> with a success message and the <strong>OrderId.</strong> If there’s an error, the code catches it and returns a <strong>500</strong> status code along with an error message.</p>
<h3 id="heading-get-coffee-lambda-function">Get Coffee Lambda function</h3>
<p>This function retrieves all coffee items:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);
<span class="hljs-keyword">const</span> dynamoDb = <span class="hljs-keyword">new</span> AWS.DynamoDB.DocumentClient();

<span class="hljs-built_in">module</span>.exports.handler = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> params = {
    <span class="hljs-attr">TableName</span>: process.env.COFFEE_ORDERS_TABLE
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> dynamoDb.scan(params).promise();
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(result.Items)
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">error</span>: <span class="hljs-string">`Could not retrieve orders: <span class="hljs-subst">${error.message}</span>`</span> })
    };
  }
};
</code></pre>
<p>This Lambda function is responsible for retrieving all coffee orders from a DynamoDB table and exemplifies a serverless approach to retrieving data from DynamoDB in a scalable manner.</p>
<p>We again use the AWS SDK to initialize a <strong>DynamoDB.DocumentClient</strong> instance to interact with DynamoDB. The <strong>handler</strong> function constructs the <strong>params</strong> object, specifying the <strong>TableName</strong>, which is dynamically set using the <strong>COFFEE_ORDERS_TABLE</strong> environment variable.</p>
<p>The <strong>scan()</strong> method retrieves all items from the table. Again, if the operation is successful, the function returns a status code of <strong>200</strong> along with the retrieved items in JSON format. In case of an error, a <strong>500</strong> status code and an error message are returned.</p>
<h3 id="heading-update-coffee-lambda-function">Update Coffee Lambda function</h3>
<p>This function updates a coffee item by its ID:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);
<span class="hljs-keyword">const</span> dynamoDb = <span class="hljs-keyword">new</span> AWS.DynamoDB.DocumentClient();

<span class="hljs-built_in">module</span>.exports.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> requestBody = <span class="hljs-built_in">JSON</span>.parse(event.body);
  <span class="hljs-keyword">const</span> { order_id, new_status, customer_name } = requestBody;

  <span class="hljs-keyword">const</span> params = {
    <span class="hljs-attr">TableName</span>: process.env.COFFEE_ORDERS_TABLE,
    <span class="hljs-attr">Key</span>: {
      <span class="hljs-attr">OrderId</span>: order_id,
      <span class="hljs-attr">CustomerName</span>: customer_name
    },
    <span class="hljs-attr">UpdateExpression</span>: <span class="hljs-string">'SET OrderStatus = :status'</span>,
    <span class="hljs-attr">ExpressionAttributeValues</span>: {
      <span class="hljs-string">':status'</span>: new_status
    }
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> dynamoDb.update(params).promise();
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Order status updated successfully!'</span>, <span class="hljs-attr">OrderId</span>: order_id })
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">error</span>: <span class="hljs-string">`Could not update order: <span class="hljs-subst">${error.message}</span>`</span> })
    };
  }
};
</code></pre>
<p>This Lambda function handles updating the status of a specific coffee order in the DynamoDB table.</p>
<p>The <strong>handler</strong> function extracts the <strong>order_id</strong>, <strong>new_status</strong>, and <strong>customer_name</strong> from the request body. It then constructs the <strong>params</strong> object to specify the table name and the primary key for the order (using <strong>OrderId</strong> and <strong>CustomerName</strong>). The <strong>UpdateExpression</strong> sets the new status of the order.</p>
<p>In the <strong>try</strong> block, the code attempts to update the order in DynamoDB using the <strong>update()</strong> method. Once again, of course if successful, the function returns a status code of <strong>200</strong> with a success message. If an error occurs, it catches the error and returns a <strong>500</strong> status code along with an error message.</p>
<h3 id="heading-delete-coffee-lambda-function">Delete Coffee Lambda function</h3>
<p>This function deletes a coffee item by its ID:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> AWS = <span class="hljs-built_in">require</span>(<span class="hljs-string">'aws-sdk'</span>);
<span class="hljs-keyword">const</span> dynamoDb = <span class="hljs-keyword">new</span> AWS.DynamoDB.DocumentClient();

<span class="hljs-built_in">module</span>.exports.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> requestBody = <span class="hljs-built_in">JSON</span>.parse(event.body);
  <span class="hljs-keyword">const</span> { order_id, customer_name } = requestBody;

  <span class="hljs-keyword">const</span> params = {
    <span class="hljs-attr">TableName</span>: process.env.COFFEE_ORDERS_TABLE,
    <span class="hljs-attr">Key</span>: {
      <span class="hljs-attr">OrderId</span>: order_id,
      <span class="hljs-attr">CustomerName</span>: customer_name
    }
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> dynamoDb.delete(params).promise();
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Order deleted successfully!'</span>, <span class="hljs-attr">OrderId</span>: order_id })
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">error</span>: <span class="hljs-string">`Could not delete order: <span class="hljs-subst">${error.message}</span>`</span> })
    };
  }
};
</code></pre>
<p>The Lambda function deletes a specific coffee order from the DynamoDB table. In the handler function, the code parses the request body to extract the <strong>order_id</strong> and <strong>customer_name</strong>. These values are used as the primary key to identify the item to be deleted from the table. The <strong>params</strong> object specifies the table name and key for the item to be deleted.</p>
<p>In the <strong>try</strong> block, the code attempts to delete the order from DynamoDB using the <strong>delete()</strong> method. If successful, again it returns a <strong>200</strong> status code with a success message, indicating that the order was deleted. If an error occurs, the code catches it and returns a <strong>500</strong> status code along with an error message.</p>
<p>Now that we’ve explained each Lambda function, let’s set up a multi-stage CI/CD pipeline.</p>
<h2 id="heading-step-4-set-up-cicd-pipeline-multi-stage-deployments-for-dev-and-prod-environments">Step 4: Set Up CI/CD Pipeline Multi-stage Deployments for Dev and Prod Environments</h2>
<p>To set up AWS secrets in your GitHub repository, first navigate to the repository’s settings. Select <strong>Settings</strong> on the top right, then go to the bottom left and select <strong>Secrets and variables.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724352977158/9250d55a-941a-4bfd-9f7d-843e9b40d8b6.png" alt="Select &quot;Settings&quot; option in GitHub repo at top right." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, click on <strong>Actions</strong> as seen in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353027861/52692cba-1bd1-4773-9441-a080af16f513.png" alt="Select &quot;Actions&quot; option to set secret variables for GitHub Actions." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>From there, select <strong>New repository secret</strong> to create secrets.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353092604/a54b12fa-31e7-43d0-b4d5-2abe6a641181.png" alt="Select button to create new repository secret variables." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Three secrets are needed to create for your pipeline, <strong>AWS_ACCESS_KEY_ID</strong>, <strong>AWS_SECRET_ACCESS_KEY</strong>, and <strong>SERVERLESS_ACCESS_KEY</strong>.</p>
<p>Use your AWS account access key credentials for the first two variables and then the serverless access key previously saved to create the <strong>SERVERLESS_ACCESS_KEY</strong>. These secrets will securely authenticate your CI/CD pipeline as seen in the image below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353131423/5b4af7c7-ff3e-431f-a9ef-1ddf74fa9e46.png" alt="Three secret variables needed to authenticate to AWS and Serverless Framework account." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Make sure that your main branch is named “<strong>main</strong>,” as this will serve as the production branch. Next, create a new branch called “<strong>dev</strong>” for development work.</p>
<p>You can also create feature-specific branches, such as “<strong>dev/feature</strong>,” for more granular development. GitHub Actions will use these branches to deploy changes automatically, with <strong>dev</strong> representing the development environment and <strong>main</strong> representing production.</p>
<p>This branching strategy allows you to manage the CI/CD pipeline efficiently, deploying new code changes whenever there's a merge into either the dev or prod environments.</p>
<h3 id="heading-how-to-use-github-actions-to-deploy-the-yaml-file">How to Use GitHub Actions to Deploy the YAML File</h3>
<p>To automate the deployment process for the Coffee Shop API, you'll utilize GitHub Actions, which integrates with your GitHub repository.</p>
<p>This deployment pipeline is triggered whenever code is pushed to the main or dev branches. By configuring environment-specific deployments, you'll ensure that updates to the dev branch deploy to the development environment, while changes to the main branch trigger production deployments.</p>
<p>Now, let’s review the code:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">deploy-coffee-shop-api</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">dev</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">deploy:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">code</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Node.js</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v3</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">node-version:</span> <span class="hljs-string">'20.x'</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">dependencies</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        cd coffee-shop-api
        npm install
</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Serverless</span> <span class="hljs-string">Framework</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span> <span class="hljs-string">-g</span> <span class="hljs-string">serverless</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">AWS</span> <span class="hljs-string">(Dev)</span>
      <span class="hljs-attr">if:</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/dev'</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        cd coffee-shop-api
        npx serverless deploy --stage dev
</span>      <span class="hljs-attr">env:</span>
        <span class="hljs-attr">AWS_ACCESS_KEY_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCESS_KEY_ID</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">AWS_SECRET_ACCESS_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_ACCESS_KEY</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">SERVERLESS_ACCESS_KEY:</span> <span class="hljs-string">${{secrets.SERVERLESS_ACCESS_KEY}}</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">AWS</span> <span class="hljs-string">(Prod)</span>
      <span class="hljs-attr">if:</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/main'</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        cd coffee-shop-api
        npx serverless deploy --stage prod
</span>      <span class="hljs-attr">env:</span>
        <span class="hljs-attr">AWS_ACCESS_KEY_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_ACCESS_KEY_ID</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">AWS_SECRET_ACCESS_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_ACCESS_KEY</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">SERVERLESS_ACCESS_KEY:</span> <span class="hljs-string">${{secrets.SERVERLESS_ACCESS_KEY}}</span>
</code></pre>
<p>The GitHub Actions YAML configuration is what automates the deployment process of the Coffee Shop API to AWS using the Serverless Framework. The workflow triggers whenever changes are pushed to the main or dev branches.</p>
<p>It begins by checking out the repository’s code, then setting up Node.js with version 20.x to match the runtime used by the Lambda functions. After that, it installs the project dependencies by navigating to the <strong>coffee-shop-api</strong> directory and running <strong>npm install</strong>.</p>
<p>The workflow also installs the Serverless Framework globally, allowing the serverless CLI to be used for deployments. Depending on which branch is updated, the workflow conditionally deploys to the appropriate environment.</p>
<p>If the changes are pushed to the dev branch, it deploys to the dev stage. If they are pushed to the main branch, it deploys to the prod stage. The deployment commands, <code>npx serverless deploy --stage dev</code> or <code>npx serverless deploy --stage prod</code> are executed within the coffee-shop-api directory.</p>
<p>For a secure deployment, the workflow accesses AWS credentials and the Serverless access key via environment variables stored in GitHub Secrets. This allows the CI/CD pipeline to authenticate with AWS and the Serverless Framework without exposing sensitive information in the repository.</p>
<p>Now, we can proceed to test out the pipeline.</p>
<h2 id="heading-step-5-test-the-dev-and-prod-pipelines">Step 5: Test the Dev and Prod Pipelines</h2>
<p>First, you'll need to verify that the main (prod) branch is called “<strong>main</strong>”. Then create a dev branch called “<strong>dev</strong>”. Once you make any valid changes to the dev branch, commit them to trigger the GitHub Actions pipeline. This will automatically deploy the updated resources to the development environment. After verifying everything in dev, you can then merge the dev branch into the main branch.</p>
<p>Merging changes into the main branch also automatically triggers the deployment pipeline for the production environment. This way, all necessary updates are applied and production resources are deployed seamlessly.</p>
<p>You can monitor the deployment process and review detailed logs of each GitHub Actions run by navigating to the <strong>Actions</strong> tab in your GitHub repository.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353173167/f1775dbc-732c-432d-9ee0-9572b8b9908f.png" alt="Select &quot;Actions&quot; in the top right of GitHub repository options." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The logs provide visibility into each step of the pipeline, helping you verify that everything is working as expected.</p>
<p>You can select any build run to review detailed logs for both the development and production environment deployments so you can track the progress and ensure that everything is running smoothly.</p>
<p>Navigate to the specific build run in GitHub Actions, as demonstrated in the image below. There, you can view the execution details and outcomes for either the development or production pipelines.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353205715/dd221126-4fed-4032-8b51-e883f1177173.png" alt="Pipeline run logs for the different branch environments (main, dev)" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Make sure to thoroughly test both the development and production environments to confirm successful pipeline executing.</p>
<h2 id="heading-step-6-test-and-validate-prod-and-dev-apis-using-postman">Step 6: Test and Validate Prod and Dev APIs using Postman</h2>
<p>Now that the APIs and resources are deployed and configured, we need to locate the unique API endpoints (URLs) generated by AWS to begin making requests to test functionality.</p>
<p>These URLs can test the API functionality by simply pasting them into a web browser. The API URLs are found in the output results of your CI/CD build.</p>
<p>To retrieve them, navigate to the GitHub Actions logs, select the most recent environment’s successful build, and click <strong>deploy</strong> to check the deployment details for the generated API endpoints.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353236275/7cbed3e1-d65a-4fa6-9dff-9974d1c2022a.png" alt="&quot;Deploy&quot; button that allows you to view log details." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click on the <strong>Deploy to AWS</strong> stage for the selected environment (Prod or Dev) in your GitHub Actions logs. Once there, you’ll find the generated API URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353272312/43eee369-618f-45f9-b9aa-6ffb6e19061b.png" alt="Detailed logs of a specific build run to review for errors or success." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Copy and save this URL, as it will be needed when testing your API’s functionality. This URL is your gateway to verifying that the deployed API works as expected.</p>
<p>Now copy one of the generated API URLs and paste it into your browser. You will see an empty array or list displayed in the response. This actually confirms that the API is functioning correctly and that you are successfully retrieving data from the DynamoDB table.</p>
<p>Even though the list is empty, it indicates that the API can connect to the database and return information.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353307388/23791725-71d7-4b1d-908c-c0f5e0fb073b.png" alt="Empty list result when inserting API URL in browser." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To verify that your API works across both environments, repeat the steps for the other API environment (Prod and Dev).</p>
<p>For more comprehensive testing, we’ll use Postman to test all the API methods, <strong>Create</strong>, <strong>Read</strong>, <strong>Update</strong> and <strong>Delete</strong>, and perform these tests for both the development and production environments.</p>
<p>To test the <strong>GET</strong> method, use Postman to send a GET request to the API’s endpoint using the URL. You will receive the same response, an empty list of coffee orders as seen in the bottom of the image below. This confirms the API’s ability to retrieve data successfully, as shown in the image below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353336998/17fff84a-a784-464f-a89e-9c73f3e863a0.png" alt="Testing the GET method using Postman." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To actually create an order, let’s test the <strong>POST</strong> method. Use Postman again to make a POST request to the API endpoint, providing the customer’s name and coffee blend in the request body, as show below :</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"customer_name"</span>: <span class="hljs-string">"REXTECH"</span>,
  <span class="hljs-attr">"coffee_blend"</span>: <span class="hljs-string">"Black"</span>
}
</code></pre>
<p>The response will be a success message with a unique OrderId of the order placed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353370197/4f3ab8df-4f1f-4c66-888c-4069b60151f9.png" alt="Testing the POST method using Postman." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Verify that the new order was saved in the DynamoDB table by reviewing the items in the environments specific table :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353402967/afbd2080-b66f-46ac-ac79-24a4d360871d.png" alt="Verifying new order is stored in DynamoDB table." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To test the <strong>PUT</strong> method, make a PUT request to the API endpoint by providing the previous order ID and a new order status in the request body as shown below :</p>
<pre><code class="lang-json">{                                                 
  <span class="hljs-attr">"order_id"</span>: <span class="hljs-string">"42a81c27-1421-4025-9bef-72b14e723c34"</span>,
  <span class="hljs-attr">"new_status"</span>: <span class="hljs-string">"Ready"</span>,                                             
  <span class="hljs-attr">"customer_name"</span>: <span class="hljs-string">"REXTECH"</span>                                             
}
</code></pre>
<p>The response will be a successful order update message with the OrderId of the order placed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353432881/f5354746-9b42-4fc9-bb70-5c18f076ecea.png" alt="Testing the PUT method using Postman." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can also verify that the order status was updated from the DynamoDB table item.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353463923/e6a2978c-bbb5-49c0-9b94-36ea404b8c11.png" alt="Verifying order status update in DynamoDB table." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To test the <strong>DELETE</strong> method, using Postman, make a DELETE request providing the previous order ID and the customer name in the request body as shown below:</p>
<pre><code class="lang-plaintext">{                                                 
  "order_id": "42a81c27-1421-4025-9bef-72b14e723c34",
  "customer_name": "REXTECH"
}
</code></pre>
<p>The response will be a successful order deleted message with the order ID of the order placed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353509090/e61a8ab8-7ce3-44b1-a122-34d29b5a5734.png" alt="Testing the DELETE method using Postman." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Again, you can verify that the order has been deleted in the DynamoDB table.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353541300/d6ed82aa-12ca-4cc2-9b0b-1b86be9557ee.png" alt="Verifying empty items in DynamoDB table." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That’s it – congratulations! You’ve successfully completed all the steps. We’ve built a serverless REST API that supports CRUD (<strong>Create, Read, Update, Delete)</strong> functionality with API Gateway, Lambda, DynamoDB, Serverless Framework and Node.js, automating deployment of approved code changes with Github Actions.</p>
<p>If you’ve gotten this far, <strong>thanks for reading!</strong> I hope it was worthwhile to you.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724353582971/091ac912-1d87-4179-addc-cc81a90c8657.png" alt="091ac912-1d87-4179-addc-cc81a90c8657" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><a target="_blank" href="https://www.linkedin.com/in/ifeanyi-otuonye/">Ifeanyi Otuonye</a> is a 6X AWS Certified Cloud Engineer skilled in DevOps, Technical Writing and instructional expertise as a Technical Instructor. He is motivated by his eagerness to learn and develop and thrives in collaborative environments. Before transitioning to the Cloud, he spend six years as a Professional Track and Field athlete.</p>
<p>In the early 2022, he strategically embarked on an mission to be a Cloud/DevOps Engineer through self study and joining a 6 month accelerated Cloud program.</p>
<p>In May 2023, he accomplished that goal and landed his first Cloud Engineering role and has now set another personal mission to empower other individuals on their journey to the Cloud.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Serverless Node.js Tutorial ]]>
                </title>
                <description>
                    <![CDATA[ The shift towards serverless architectures is rapidly becoming a pivotal aspect of application development. Even Node.js, which is traditionally used with servers, can be used to build a serverless application. We just published a course on the freeC... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/serverless-node-js-tutorial/</link>
                <guid isPermaLink="false">66b2066543f24c1bb1598189</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Wed, 28 Feb 2024 20:59:09 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/sn1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The shift towards serverless architectures is rapidly becoming a pivotal aspect of application development. Even Node.js, which is traditionally used with servers, can be used to build a serverless application.</p>
<p>We just published a course on the freeCodeCamp.org YouTube channel that will teach you how to develop serverless applications using Node.js. You'll learn the nuances of deploying an Express.js and Node.js application to AWS Lambda. </p>
<p>You'll also learn how to leverage the cutting-edge capabilities of <a target="_blank" href="https://neon.tech/education">Neon Serverless Postgres</a> (which created a grant for freeCodeCamp to help make this course possible) and the Serverless Framework to enhance your application development and deployment strategy.</p>
<p>Justin Mitchel created this course. He is a popular instructor and creator of the Coding for Entrepreneurs course platform. Justin brings his extensive expertise to the table, ensuring that you not only grasp the theoretical aspects but also gain hands-on experience through a project-based learning approach. </p>
<p>Here are some of the key sections in this course.</p>
<ul>
<li><strong>What Serverless Means for App Development</strong>: A foundational overview, preparing learners for the paradigm shift in how applications are built and deployed.</li>
<li><strong>Requirements &amp; Tech Overview</strong>: Familiarize yourself with the tools and technologies that will be utilized throughout the course.</li>
<li><strong>Project Setup to Deployment</strong>: From initial setup to running Express locally with the Serverless Framework, this course ensures a hands-on approach to mastering serverless deployment.</li>
<li><strong>Securing and Managing Your Deployment</strong>: Learn to secure your deployment with AWS System Manager Parameter Store and manage Neon resources efficiently with the Neon CLI.</li>
<li><strong>Database Integration and Management</strong>: Dive deep into <a target="_blank" href="https://neon.tech/education">integrating NodeJS with Neon Postgres</a>, managing database schemas, and automating branched Neon database secrets.</li>
<li><strong>Advanced Deployment Techniques</strong>: Master automated deployments via GitHub Actions and explore integration with rewrites in Next.js and Vercel, culminating in deploying Express.js to Vercel.</li>
</ul>
<p>Serverless architectures offer unparalleled scalability and cost efficiency, allowing developers to focus on code rather than managing servers. This approach not only streamlines deployment processes but also significantly reduces operational costs, as you only pay for the compute time you use, making it an ideal solution for projects of any size.</p>
<p>Watch the full course on <a target="_blank" href="https://www.youtube.com/watch?v=cxgAN7T3rq8">the freeCodeCamp.org YouTube channel</a> (4-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/cxgAN7T3rq8" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Serverless Architecture Patterns and Best Practices ]]>
                </title>
                <description>
                    <![CDATA[ By Faith Oyama Serverless architecture has become a hot topic in the developer world, and for good reason.  It promises a paradigm shift – one where we leave behind the burdens of server management and focus solely on building and deploying code. No ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/serverless-architecture-patterns-and-best-practices/</link>
                <guid isPermaLink="false">66d45ef28812486a37369ccb</guid>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 09 Jan 2024 01:07:58 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/01/Light-Blue-Futuristic-Technology-Project-Proposal-Presentation.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Faith Oyama</p>
<p>Serverless architecture has become a hot topic in the developer world, and for good reason. </p>
<p>It promises a paradigm shift – one where we leave behind the burdens of server management and focus solely on building and deploying code. No more provisioning VMs, patching software, or scaling infrastructure manually. </p>
<p>In this article, we'll simplify the world of serverless, exploring its core principles and benefits. We'll see how serverless applications scale effortlessly, adapt to changing workloads, and potentially save you precious resources. </p>
<p>But, like any powerful tool, serverless comes with its considerations. We'll shed light on potential challenges like cold starts and vendor lock-in, empowering you to make informed decisions before embarking on your serverless journey.</p>
<p>This article aims to equip you with the knowledge and best practices to become a confident serverless developer. We'll break down complex concepts into clear, actionable steps, using real-world examples to illustrate key points. Whether you're a curious beginner or a seasoned developer looking to expand your skill set, this article will serve as your comprehensive guide to unlocking the potential of serverless architecture.</p>
<h2 id="heading-common-serverless-architecture-patterns">Common Serverless Architecture Patterns</h2>
<p>Now that we've laid the groundwork for serverless, let's explore some practical ways to build your serverless applications. Buckle up, because we're entering the realm of patterns, and reusable designs that help you structure your code efficiently and leverage the strengths of serverless architecture.</p>
<h3 id="heading-api-gateway-amp-lambda-the-dynamic-duo">API Gateway &amp; Lambda: The Dynamic Duo</h3>
<p>This is the classic serverless combo, like peanut butter and jelly for web APIs. Think of API Gateway as your friendly neighbourhood receptionist, greeting incoming requests from various sources (web browsers, mobile apps, and so on). With a polite nod, it then routes each request to the appropriate Lambda function (think processing data, sending emails, or updating databases). </p>
<p>It's a seamless partnership: the Gateway handles routing and security, while Lambda focuses on your application's specific logic.</p>
<p>Here are some perks of this pattern:</p>
<ul>
<li><strong>Rapid Deployments:</strong> Get your APIs up and running quickly without worrying about server infrastructure.</li>
<li><strong>Scalability on Demand:</strong> Lambda functions automatically scale based on traffic, freeing you from manually adjusting server capacity.</li>
<li><strong>Pay-per-Use Pricing:</strong> You only pay for the resources your code uses, making serverless cost-effective for applications with fluctuating traffic.</li>
</ul>
<p>But remember, even dynamic duos have their quirks. Cold starts, in which the initial invocation of a Lambda function takes longer to complete, can affect initial response times. </p>
<p>And while API Gateway offers strong security features, you still need to implement proper authorization and validation within your Lambda functions.</p>
<h3 id="heading-fan-out-pattern">Fan-Out Pattern</h3>
<p>Need to handle massive workloads but don't want to wait in line? Enter the Fan-Out pattern. </p>
<p>Imagine a single event (like a large file upload) triggering a swarm of Lambda functions working simultaneously on smaller chunks of the task. It's like having a team of chefs tackling different courses of a complex meal, making the entire process much faster.</p>
<p>This pattern excels in scenarios like:</p>
<ul>
<li>Image resizing: Break down a large image into smaller parts for parallel resizing, then stitch them back together for a fast and efficient outcome.</li>
<li>Email sending: Send bulk emails to thousands of recipients without bogging down your system by distributing the task among multiple Lambda functions.</li>
</ul>
<p>But remember that with great power comes great responsibility. Managing dependencies between your parallel functions and ensuring smooth data consistency can be tricky. Consider using queues or streams to coordinate their work and avoid unwanted surprises.</p>
<h3 id="heading-messaging-pattern">Messaging Pattern</h3>
<p>Ever feel like your code components are tangled in a spaghetti dinner of dependencies? The Messaging pattern comes to the rescue, introducing a layer of calm, asynchronous communication between your serverless functions. </p>
<p>Instead of functions directly calling each other, they simply send messages to a queue (like a virtual mailbox). The functions responsible for processing those messages can pick them up at their own pace, decoupling them from the sender's execution time.</p>
<p>Think of it like leaving an order at a restaurant: tell the kitchen what you want (send a message), and then relax – your food will arrive (the message will be processed) when it's ready, without you needing to keep checking on the chef. </p>
<p>This approach offers several benefits:</p>
<ul>
<li><strong>Agility:</strong> If one function fails, the message remains in the queue for later processing, preventing cascading failures.</li>
<li><strong>Scalability:</strong> Scale your message processing functions independently from the sending functions for optimal performance.</li>
<li><strong>Flexibility:</strong> Decoupled components are easier to maintain and update, making your application more agile.</li>
</ul>
<p>But remember, choosing the right queueing service and managing message backlogs requires careful consideration. Make sure your messaging system can handle your application's expected workload and provides efficient message buffering to avoid bottlenecks.</p>
<h2 id="heading-serverless-best-practices">Serverless Best Practices</h2>
<h3 id="heading-function-focus">Function Focus</h3>
<p>Keep your Lambda functions small, focused, and stateless. Think of them as single-purpose spells: each should handle a specific task and avoid holding onto any persistent state. This improves their scalability and makes them easier to debug and maintain.</p>
<h3 id="heading-error-handling">Error Handling</h3>
<p>Nobody likes unexpected crashes. Handle errors within your functions and log them efficiently. Use tools like <a target="_blank" href="https://aws.amazon.com/cloudwatch/">CloudWatch</a> to monitor logs and proactively identify potential issues before they become full-blown serverless storms.</p>
<h3 id="heading-observability-is-key">Observability is Key</h3>
<p>You need tools to observe your serverless application's health and performance. Utilize monitoring services like <a target="_blank" href="https://prometheus.io/">Prometheus</a> or <a target="_blank" href="https://www.datadoghq.com/">Datadog</a> to track metrics like execution time, memory usage, and invocations. Early detection of performance bottlenecks helps you optimize your functions and keep your costs in check.</p>
<h3 id="heading-testing-testing-1-2-3">Testing, Testing, 1, 2, 3</h3>
<p>Don't send your functions into the serverless void untested! Rigorous unit and integration testing are crucial for catching bugs and ensuring your code behaves as expected. Frameworks like <a target="_blank" href="https://jestjs.io/">Jest</a> and Serverless Framework can simplify your testing process and prevent unexpected serverless hiccups.</p>
<h3 id="heading-cost-optimization">Cost Optimization</h3>
<p>Remember, with great power comes great responsibility for your serverless wallet. Utilize cost-saving features like throttling, which limits function invocations per second, and timeouts, which automatically terminate long-running executions. Pay-per-use billing can be your friend, but only if you manage it wisely.</p>
<h3 id="heading-security-first">Security First</h3>
<p>Don't let your serverless application fall to malicious attacks. Implement IAM roles and policies to control access to resources and functions. Use encryption for sensitive data and regularly review your security posture to ensure your serverless spells remain protected from dark magic.</p>
<h3 id="heading-logging-and-tracing">Logging and Tracing</h3>
<p>Enabling granular logging within your functions helps you troubleshoot issues and understand their execution flow. Use tracing tools like <a target="_blank" href="https://aws.amazon.com/xray/">X-Ray</a> to visualize the path of invocations across your serverless components, making debugging a breeze even in the most complex serverless landscapes.</p>
<h3 id="heading-versioning-and-deployment">Versioning and Deployment</h3>
<p>Continuous improvement is key in the serverless world. Utilize CI/CD pipelines to automate build, test, and deployment processes for your functions. Versioning allows you to roll back to stable versions if needed and experiment with new features without impacting your live application.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Now, it's time to put to test your newfound knowledge and build incredible applications that scale with ease and cost less.</p>
<p>To always stay ahead of the curve, consider exploring these resources:</p>
<ul>
<li><strong><a target="_blank" href="https://aws.amazon.com/serverless/sam/">AWS Serverless Application Model (SAM)</a>:</strong> Simplify building and deploying serverless applications on AWS.</li>
<li><strong><a target="_blank" href="https://github.com/serverless/serverless">Serverless Framework</a>:</strong> An open-source framework for building and deploying serverless applications across various cloud providers.</li>
<li><strong><a target="_blank" href="https://www.meetup.com/pro/serverless/">Serverless Meetups and Conferences</a>:</strong> Connect with other serverless enthusiasts and learn from the experts.</li>
</ul>
<p>As you continue your serverless exploration, remember the golden rule: experiment, share your knowledge, and have fun. And if you ever encounter a particularly tricky serverless riddle, reach out! The serverless community is always eager to help.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Integrate AI into Your Serverless App With Amazon Bedrock ]]>
                </title>
                <description>
                    <![CDATA[ By Sam Williams In today's tech landscape, integrating AI is no longer a luxury – it's a necessity.  AI-driven applications have the potential to transform user experiences, automate complex tasks, and unlock new realms of possibilities. Understandin... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/ai-in-your-serverless-app-amazon-bedrock/</link>
                <guid isPermaLink="false">66d460c73bc3ab877dae222a</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 02 Oct 2023 19:23:14 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-10-02-at-14.19.28.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Sam Williams</p>
<p>In today's tech landscape, integrating AI is no longer a luxury – it's a necessity. </p>
<p>AI-driven applications have the potential to transform user experiences, automate complex tasks, and unlock new realms of possibilities. Understanding and leveraging AI APIs is a pivotal skill for developers looking to stay at the forefront of innovation.</p>
<h2 id="heading-brief-overview-of-ai-apis">Brief Overview of AI APIs</h2>
<p>Artificial Intelligence APIs are powerful tools that allow developers to tap into the capabilities of pre-trained machine learning models. These APIs expose functionalities like natural language processing, computer vision, and more, enabling developers to easily incorporate advanced AI capabilities into their applications.</p>
<p>You no longer have to understand training epochs and neural network architecture to use AI in your projects and build incredibly powerful features for your users.</p>
<h3 id="heading-the-purpose-of-this-tutorial">The purpose of this tutorial:</h3>
<p>The goal of this tutorial is to equip you with the knowledge and practical skills you need to seamlessly integrate AI APIs into your projects.</p>
<p>I'll walk you through the entire process, from choosing the right API for your specific needs to hands-on implementation and best practices for seamless integration. </p>
<p>By the end, you'll be well-equipped to infuse AI-powered intelligence into your applications, opening up a world of new possibilities.</p>
<p>So, let's embark on this journey together and unlock the true potential of AI APIs.</p>
<h2 id="heading-current-ai-api-options">Current AI API Options</h2>
<p>There are more and more AI services available through a simple API than ever before. In this article we’ll be using <a target="_blank" href="https://aws.amazon.com/bedrock/">Amazon Bedrock</a>, but there are loads more out there. Even Amazon Bedrock has 6 models available, with more coming in the future.</p>
<h3 id="heading-comparing-available-ai-apis">Comparing available AI APIs</h3>
<p>To help you make an informed decision, let's compare some of the leading AI APIs available in the market. Below is a comparison table of some prominent options:</p>
<table>
<thead>
<tr>
<th>API</th>
<th>Description</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>GPT-3.5 (16k)</td>
<td>Cutting-edge language model that can understand as well as generate natural language or code</td>
<td>$0.0003/ 1000 input tokens
    $0.004/ 1000 output tokens</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>GPT-4 (32K)</td>
<td>OpenAI’s most advanced system, producing safer and more useful responses</td>
<td>$0.06/ 1000 input tokens
    $0.12/ 1000 output tokens</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>A2I Jurassic-2 Mid model (Bedrock)</td>
<td>Mid-sized model, designed to strike the right balance between exceptional quality and affordability</td>
<td>$0.0125/ 1000 input tokens
    $0.0125/ 1000 output tokens</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>A2I Jurassic-2 Ultra model (Bedrock)</td>
<td>AI21’s most powerful model, offering exceptional quality</td>
<td>$0.0188/ 1000 input tokens
    $0.0188/ 1000 output tokens</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Anthropic Claude Instant (Bedrock)</td>
<td>Cutting-edge general purpose large language model</td>
<td>$0.00163/ 1000 input tokens
    $0.01102/ 1000 output tokens</td>
<td></td>
</tr>
<tr>
<td>Stability AI (Bedrock)</td>
<td>Image Generation</td>
<td>$0.018 - $0.072 per image
depending on size and quality</td>
</tr>

</tbody>
</table>

<h3 id="heading-key-factors-to-consider-when-selecting-an-api">Key factors to consider when selecting an API</h3>
<p>When choosing an AI API for your project, it's crucial to consider several key factors:</p>
<ol>
<li><strong>API capabilities and features:</strong> Assess the specific functionalities offered by the API and ensure they align with your project requirements. The quality of the generated content can also vary a lot between models, so it's a good idea to test them out and see how well they perform for your use case.</li>
<li><strong>Scalability and performance:</strong> Evaluate the API's ability to handle varying workloads and ensure it meets your performance expectations, especially during peak usage.</li>
<li><strong>Cost considerations:</strong> Understand the pricing model of the API, including any associated costs for usage, and determine its compatibility with your budget. </li>
<li><strong>Data privacy and security:</strong> Ensure that the API provider complies with data protection regulations and has robust security measures in place to safeguard sensitive information.</li>
</ol>
<p>By taking these factors into account, you'll be better equipped to choose the AI API that best suits the needs of your project.</p>
<p>For this tutorial we’re going go with Amazon Bedrock using the <a target="_blank" href="https://aws.amazon.com/bedrock/jurassic/">A2I Jurassic-2 Mid model.</a></p>
<h2 id="heading-how-to-request-model-access">How to Request Model Access</h2>
<p>As this service is brand new, you have to request access to the models you want to use. </p>
<p>To do this, log into your AWS account, Search for “Bedrock” and then select the “Base models” tab on the left. Mouse over any model and it’ll say that you don’t currently have access and to request access in “Model Access”. </p>
<p>This lists all of the models. Click the edit button in the top right, select the models you want to have access to and then click “Save”. For this you need to select <code>Jurassic-2 Ultra</code> and <code>Jurassic-2 Mid</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-09-30-at-18.12.43.png" alt="Image" width="600" height="400" loading="lazy">
<em>Select models for which you'd like to request access</em></p>
<p>This should only take a minute or two to be approved but best to do it ASAP.</p>
<h2 id="heading-project-build-a-holiday-planning-api">Project: Build a Holiday Planning API</h2>
<h3 id="heading-what-our-api-will-do">What Our API Will Do</h3>
<p>Our API is designed to simplify holiday planning. By providing the state code of your destination and the duration of your visit, we'll generate a personalised itinerary, suggesting the best activities and places to explore.</p>
<h3 id="heading-how-to-set-up-the-repo">How to set up the Repo</h3>
<p>We'll be using the Serverless Framework for this project. If you've never used it before then you can follow this quick <a target="_blank" href="https://completecoding.io/how-to-deploy-to-aws-from-the-serverless-framework/">tutorial to install Serverless</a> and get everything set up.</p>
<p>We're going to be using JavaScript for this project so create a new repo like this:</p>
<pre><code>sls create --template aws-nodejs --path aiTourGuide
</code></pre><h3 id="heading-create-a-lambda-with-comments">Create a Lambda with comments</h3>
<p>We need to start by creating our Lambda function. I like to store mine under <code>/src/functions/{functionName}/index.js</code>. In this case my functionName will be <code>aiTourGuide</code>.</p>
<p>In the new <code>index.js</code> file we can start with this code. It tries to get the state and the duration from the request and then returns a response.</p>
<pre><code class="lang-js"><span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { state_code, duration } = <span class="hljs-built_in">JSON</span>.parse(event.body);

    <span class="hljs-comment">// Code for generating itinerary will go here</span>

    <span class="hljs-keyword">const</span> response = {
        <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'Itinerary generated successfully!'</span>)
    };

    <span class="hljs-keyword">return</span> response;
};
</code></pre>
<h3 id="heading-sign-up-to-the-national-park-service-api">Sign up to the National Park Service API</h3>
<p>Now we want to get some data to pass to the AI. We could just ask it to generate an itinerary for us, but giving it specific data to work with usually ends up with a much better result.</p>
<ol>
<li>Visit the <a target="_blank" href="https://www.nps.gov/subjects/developer/get-started.htm">National Park Service API website</a> and sign up for an API key.</li>
<li>Once registered, you'll receive an API key by email to access their services.</li>
</ol>
<h3 id="heading-add-the-national-park-service-api-call-to-the-lambda">Add the National Park Service API Call to the Lambda</h3>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> axios = <span class="hljs-built_in">require</span>(<span class="hljs-string">'axios'</span>);
<span class="hljs-keyword">const</span> parksApiKey = process.env.parksApiKey

<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { state_code, duration } = <span class="hljs-built_in">JSON</span>.parse(event.body);

    <span class="hljs-comment">// Make a request to the National Park Service API</span>
    <span class="hljs-keyword">const</span> parksApiUrl = <span class="hljs-string">`https://developer.nps.gov/api/v1/parks?stateCode=<span class="hljs-subst">${state_code}</span>&amp;api_key=<span class="hljs-subst">${parksApiKey}</span>`</span>

    <span class="hljs-keyword">const</span> parksResponse = <span class="hljs-keyword">await</span> axios.get(parksApiUrl);

    <span class="hljs-comment">// Extract relevant data from the response</span>
    <span class="hljs-keyword">const</span> parks = parksResponse.data.data.map(<span class="hljs-function"><span class="hljs-params">park</span> =&gt;</span> {
        <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">name</span>: park.fullName,
            <span class="hljs-attr">description</span>: park.description
        };
    });

    <span class="hljs-comment">// Code for generating itinerary with park data will go here</span>

    <span class="hljs-keyword">const</span> responseBody = parks;
    <span class="hljs-keyword">const</span> response = {
        <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(responseBody)
    };

    <span class="hljs-keyword">return</span> response;
};
</code></pre>
<p>We're making the request to the parks API using Axios, then getting just the <code>name</code> and <code>description</code> of each park from the response. For now we are just going to return that data in the API to see what we get.</p>
<p>One thing we're doing is to get the <code>parksApiKey</code> from the environment variables at the start of the file. To add the <code>parksApiKey</code> as an environment variable in a Serverless Framework <code>serverless.yml</code> file, you can follow these steps:</p>
<ol>
<li>Open your <code>serverless.yml</code> file in a text editor.</li>
<li>Locate the <code>provider</code> section, which defines the AWS provider settings. Under it, add an <code>environment</code> block if it doesn't already exist.</li>
<li>Within the <code>environment</code> block, define your environment variable like this:</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">provider:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">aws</span>
  <span class="hljs-attr">runtime:</span> <span class="hljs-string">nodejs18.x</span>
  <span class="hljs-attr">environment:</span>
    <span class="hljs-attr">parksApiKey:</span> <span class="hljs-string">"YOUR API KEY"</span>
</code></pre>
<h3 id="heading-configure-the-serverlessyml-config">Configure the Serverless.yml config</h3>
<p>To actually deploy an API and our code, we need to tell Serverless what to deploy. We do this by changing the <code>functions</code> section of the config.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">functions:</span>
  <span class="hljs-attr">aiTourGuide:</span>
    <span class="hljs-attr">handler:</span> <span class="hljs-string">src/functions/aiTourGuide/index.handler</span>
    <span class="hljs-attr">events:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">httpApi:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">/tourguide</span>
          <span class="hljs-attr">method:</span> <span class="hljs-string">post</span>
</code></pre>
<p>This means we’ll be deploying a <code>aiTourGuide</code> lambda function with a post API endpoint pointing at <code>/tourguide</code>. Just make sure that the handler section is the correct path for your repo and folder structure.</p>
<p>If you have <a target="_blank" href="https://completecoding.io/aws-credentials-setup/">configured your AWS credentials to a specific profile</a>, you need to add that to your provider section, otherwise it will use your default AWS credentials.</p>
<pre><code class="lang-jsx">provider:
  name: aws
  <span class="hljs-attr">runtime</span>: nodejs18.x
  <span class="hljs-attr">profile</span>: <span class="hljs-string">"Your Profile"</span> <span class="hljs-comment">// optional</span>
</code></pre>
<h3 id="heading-deploy-and-test">Deploy and test</h3>
<p>Now that we've created our Lambda function and integrated the National Park Service API, it's time to deploy and test our holiday planning API.</p>
<ol>
<li><strong>Deployment:</strong> All we need to do is run <code>sls deploy</code> again and our changes will be deployed.</li>
<li><strong>Testing</strong>: Use a tool like Postman to send a POST request to your API with the required parameters, such as <code>state_code</code> and <code>duration</code>. You should get a response like this. </li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/Screenshot-2023-09-30-at-15.56.11.png" alt="Image" width="600" height="400" loading="lazy">
<em>Image showing the response</em></p>
<p>You can see we have an array of objects, with the name of the park and the description. Exactly what we wanted.</p>
<h3 id="heading-how-to-prepare-our-ai-prompt">How to prepare our AI prompt</h3>
<p>Next, we'll prepare a request to an AI API to enhance our holiday planning recommendations. We'll be using the A2I Jurassic-2 Mid model using Amazon Bedrock to generate engaging descriptions for the recommended activities.</p>
<p>I tend to start relatively simple and refining the prompt as I see how it works. I also wrap my prompt generation in a function. This can get quite large and complex later on, so it’s nicer not having it in the main handler. I often have it in it’s own file! </p>
<p>Lets start with something like this:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> generatePrompt = <span class="hljs-function">(<span class="hljs-params">{parks,duration}</span>) =&gt;</span> {

    <span class="hljs-keyword">const</span> stringListOfParks = parks.map(<span class="hljs-function">(<span class="hljs-params">{name, description}</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">`Park Name: <span class="hljs-subst">${name}</span>:
    description: <span class="hljs-subst">${description}</span>`</span>}).join(<span class="hljs-string">`

    `</span>)

    <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`You are an expert tour guide in the US who focusses on designing holiday itinararies for spending time in the national parks. 
    I am going to give you descriptions of multiple parks in the area as well as the duration of the trip. 
    Create an itinerary for this trip, outlining what activities can bo done on each day.

    Trip duration = <span class="hljs-subst">${duration}</span> days

    Local national parks:
    <span class="hljs-subst">${stringListOfParks}</span>
    `</span>;
    <span class="hljs-keyword">return</span> prompt
}
</code></pre>
<p>The <code>stringListOfParks</code> function turns the object array into a long string. This might not be necessary but we’ll have to wait and see.</p>
<p>Then we create the AI prompt. We tell the AI who they are supposed to be, what information we’re going to give them, and what we want them to do. To start with this is fine, but over time we can test different changes to our prompt to see what generates the best results.</p>
<h3 id="heading-how-to-call-the-ai-api">How to call the AI API</h3>
<p>Now that we have a prompt, we can pass this to Amazon Bedrock to handle our prompt. We need to start by importing the AWS SDK and creating the <code>bedrockruntime</code>.</p>
<p>You’ll also need to install the AWS SDK for the bedrock as it’s not currently included in any of the lambda versions:</p>
<pre><code class="lang-jsx">npm i -S @aws-sdk/client-bedrock-runtime
</code></pre>
<p>And we add this code to the top of our Lambda file.</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { BedrockRuntime } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-bedrock-runtime"</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;

<span class="hljs-keyword">const</span> bedrockruntime = <span class="hljs-keyword">new</span> BedrockRuntime()
</code></pre>
<p>We’re also using imports now, which means we need to change our <code>index.js</code> file to an <code>index.mjs</code>. If you did this using TypeScript then you wouldn't have to rename your file. </p>
<p>We need to call the <code>invokeModel</code> command and pass it a set of parameters. I find that it is cleaner to create a separate object for the params than doing it all in one place.</p>
<p>Currently there isn’t an async version of the <code>invokeModel</code> command, so we’ll wrap it in a promise.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> aiPrompt = generatePrompt({parks,duration});

<span class="hljs-keyword">const</span> aiModelId = <span class="hljs-string">'ai21.j2-mid-v1'</span>; <span class="hljs-comment">// we're using the A2I Jurassic-2 Mid model</span>

<span class="hljs-keyword">const</span> invokeModelParams = {
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">prompt</span>: aiPrompt,
        <span class="hljs-attr">maxTokens</span>: <span class="hljs-number">200</span>,
        <span class="hljs-attr">temperature</span>: <span class="hljs-number">0.5</span>,
        <span class="hljs-attr">topP</span>: <span class="hljs-number">0.5</span>, <span class="hljs-comment">// optional</span>
    }),
    <span class="hljs-attr">modelId</span>: aiModelId,
    <span class="hljs-attr">accept</span>: <span class="hljs-string">'application/json'</span>,
    <span class="hljs-attr">contentType</span>: <span class="hljs-string">'application/json'</span>
};

<span class="hljs-keyword">const</span> aiResponse = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    bedrockruntime.invokeModel(invokeModelParams, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">err, data</span>) </span>{
        <span class="hljs-keyword">if</span> (err) {
            reject(err); <span class="hljs-comment">// an error occurred</span>
        } <span class="hljs-keyword">else</span> {
            resolve(data); <span class="hljs-comment">// successful response</span>
        }
    });
});

<span class="hljs-comment">// Extract AI-generated text from the response</span>
<span class="hljs-keyword">const</span> aiResponseJson = <span class="hljs-built_in">JSON</span>.parse(
    <span class="hljs-keyword">new</span> TextDecoder().decode(aiResponse.body)
);
<span class="hljs-keyword">const</span> aiItinerary =  aiResponseJson.completions[<span class="hljs-number">0</span>].data.text;

<span class="hljs-keyword">const</span> responseBody = aiItinerary;
<span class="hljs-keyword">const</span> response = {
    <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
    <span class="hljs-attr">body</span>: responseBody,
};
<span class="hljs-keyword">return</span> response;
</code></pre>
<p>You may notice that we’re passing more than just our prompt in the body. That is because we can change a few other things to get a different output.</p>
<p>LLMs work by choosing the next word in the sentence. <code>temperature</code> and <code>topP</code> control whether the model chooses unusual words or sticks to the most likely word.</p>
<ul>
<li>Temperature: Closer to 1 means more unusual words will be chosen, closer to 0 chooses more likely words.</li>
<li>topP: When choosing the next word, limit how many options the AI has to choose from by summing up the probabilities. Numbers closer to 1 mean more unlikely words are included.</li>
</ul>
<p>In our case we want a relatively creative response but also for things to be correct, so 0.5 is a good starting setting for both. If we were asking it to describe a sci-fi scene we would want to go with temp=0.7 topP=0.8, or if we were asking it to write data processing code we would reduce it to 0.2 as we want an answer that is more likely to be correct.</p>
<p>These are both things that you can change and test to see what values give the best results. Which parameters you pass in also <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html">depends on the model</a>.</p>
<h3 id="heading-how-to-add-iam-permissions-to-call-bedrock">How to add IAM permissions to call Bedrock</h3>
<p>If your Lambda function needs to access AWS resources or services like Amazon Bedrock, we need to make sure to configure the appropriate IAM permissions. </p>
<p>In your serverless.yml file you need to add this to your provider section. This says that this Lambda has permission to use <code>bedrock:InvokeModel</code>. </p>
<pre><code class="lang-jsx">provider:
  name: aws
  <span class="hljs-attr">runtime</span>: nodejs18.x
  <span class="hljs-attr">environment</span>:
    parksApiKey: YOUR API KEY
  <span class="hljs-attr">iam</span>:
    role:
      statements:
        - Effect: <span class="hljs-string">"Allow"</span>
          <span class="hljs-attr">Action</span>:
            - <span class="hljs-string">"bedrock:InvokeModel"</span>
          <span class="hljs-attr">Resource</span>: <span class="hljs-string">"*"</span>
</code></pre>
<h3 id="heading-deploy-and-test-again">Deploy and test (again)</h3>
<p>After integrating the AI API and ensuring the proper IAM permissions, redeploy your Lambda function by running <code>sls deploy</code>. Then we can test it once more to ensure the AI-generated holiday itinerary is working properly.</p>
<p>Using the same request as last time, this is the response I got, and you should get something similar.</p>
<p><em>Day 1:</em></p>
<ul>
<li><em>Arrive in Jackson, Mississippi and check into hotel</em></li>
<li><em>Visit Medgar and Myrlie Evers Home National Monument</em></li>
<li><em>Overnight in Jackson</em></li>
</ul>
<p><em>Day 2:</em></p>
<ul>
<li><em>Drive to Natchez, Mississippi and check into hotel</em></li>
<li><em>Visit Natchez National Historical Park</em></li>
<li><em>Overnight in Natchez</em></li>
</ul>
<p><em>Day 3:</em></p>
<ul>
<li><em>Drive to Vicksburg, Mississippi and check into hotel</em></li>
<li><em>Visit Vicksburg National Military Park</em></li>
<li><em>Overnight in Vicksburg</em></li>
</ul>
<p><em>Day 4:</em></p>
<ul>
<li><em>Drive to Tupelo, Mississippi and check into hotel</em></li>
<li><em>Visit Tupelo National Battlefield</em></li>
<li><em>Overnight in Tupelo</em></li>
</ul>
<p><em>Day 5:</em></p>
<ul>
<li><em>Drive to Corinth, Mississippi and check into hotel</em></li>
<li><em>Visit Shiloh National Military Park</em></li>
<li><em>Overnight in Corinth</em></li>
</ul>
<p><em>Day 6:</em></p>
<ul>
<li><em>Drive to Jackson, Mississippi and check into hotel</em></li>
<li><em>Visit Brices Cross Roads National Battlefield Site</em></li>
<li><em>Overnight in Jackson</em></li>
</ul>
<p><em>Day 7:</em></p>
<ul>
<li><em>Drive to Gulf Islands National Seashore and check into hotel</em></li>
</ul>
<h2 id="heading-fixes-to-the-code">Fixes to the Code</h2>
<p>There are few small issues:</p>
<ul>
<li>It cuts off half way through day 7 even though we said 8 days.</li>
<li>The descriptions aren’t very interesting.</li>
</ul>
<h3 id="heading-how-to-extend-the-token-limit">How to extend the token limit</h3>
<p>The reason that the response was cut off is that we initially passed a <code>maxTokens: 200</code> in our AI command. This should be a simple fix of increasing this number. </p>
<p>We could set it to a very high number like 10,000 but we still have to pay for all of the tokens generated. Setting it to 10,000 won’t make every response 10,000 tokens long, but having a more sensible limit protects us from having an unexpected AWS bill.</p>
<p>I’m setting mine to 1000. If you want to get fancy you could change this based on the number of days they are traveling for.</p>
<h3 id="heading-how-to-improve-the-itinerary">How to improve the itinerary</h3>
<p>This one is a bit harder. The problems are that it is very bland and it repeats a lot of “drive here and check into the hotel”, “Overnight in Y”.</p>
<p>We can try improving our prompt to get a better result. First, let's explicitly say that it doesn’t need to tell us about the driving or checking into a hotel.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// New content    </span>
Do not write about driving. Do not write about checking into hotels. Do not write about where to overnight.
</code></pre>
<p>We can also ask for a more descriptive result. I added another line to the prompt:</p>
<pre><code class="lang-jsx">Give a description <span class="hljs-keyword">of</span> the things they will see and what there is to <span class="hljs-keyword">do</span> <span class="hljs-keyword">in</span> each park.
</code></pre>
<p>One trick for redeploying when you have only changed code is function deployments. You can run <code>sls deploy function -f {function name}</code> which in our case is <code>sls deploy function -f aiTourGuide</code>. This is a lot quicker than redeploying the whole app, allowing you to test sooner and therefore itterate quicker.</p>
<h3 id="heading-failing-to-improve">Failing to improve</h3>
<p>Having made this change, I hoped that the result would improve, but it didn’t. I tried about 15 different prompts and they all kept the same structure and ignored my instructions to not talk about hotels, driving, or staying overnight.</p>
<h3 id="heading-option-2-change-other-parameters">Option 2 – change other parameters</h3>
<p>With the AI always giving a very similar and boring response, you can start changing other things. To increase the creativity of the AI, increase the temperature. I went with 0.8.</p>
<p>This should get the AI to select more unusual words and create a less structured and more random response. </p>
<p>Unfortunately, the response was almost identical.</p>
<h3 id="heading-option-3-change-the-model">Option 3 – change the model</h3>
<p>One of the awesome things about using Bedrock is that there are multiple models to use, and switching between them can be very easy. </p>
<p>We’ve been using the <code>Jurassic-2 Mid</code> model until now, but it’s not up to this task. Maybe it is good with much smaller prompts or simpler tasks. For now we can switch to using <code>Jurassic-2 Ultra</code>. The amazing thing is this is just one line of code:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> aiModelId = <span class="hljs-string">"ai21.j2-ultra-v1"</span>; <span class="hljs-comment">// used to be ai21.j2-mid-v1</span>
</code></pre>
<p>Because they’re from the same company, they have identical parameters. If you wanted to switch to one of the other models you might have to change a few other options in the body of <code>invokeModelParams</code>.</p>
<p>One thing I do advise with this larger model is increasing the Lambda timeout. By default it is at 6s but sometimes that isn’t enough.</p>
<pre><code class="lang-jsx">functions:
  aiTourGuide:
    handler: src/functions/aiTourGuide/index.handler
    <span class="hljs-attr">timeout</span>: <span class="hljs-number">26</span>  # &lt; <span class="hljs-keyword">new</span> config line
    <span class="hljs-attr">events</span>:
      - httpApi:
          path: /tourguide
          <span class="hljs-attr">method</span>: post
</code></pre>
<h2 id="heading-the-results">The Results</h2>
<p>With our updated prompt and our new model we get a far better result:</p>
<p><em>Day 1:<br>Start your trip by exploring Brices Cross Roads National Battlefield Site. Learn about how the Confederate victory at Brices Cross Roads was a significant victory for Major General Nathan Bedford Forrest, but its long term effect on the war proved costly for the Confederates. Brices Cross Roads is an excellent example of winning the battle, but losing the war.</em></p>
<p><em>Day 2:<br>Today, visit Emmett Till and Mamie Till-Mobley National Monument. In 1955, 14-year-old Emmett Till traveled to Money, Mississippi, to visit relatives. He was kidnapped, tortured, and murdered after reportedly whistling at a white woman. His mother, Mamie Till-Mobley, insisted on an open-casket funeral near their hometown of Chicago. Her brave decision let the world see the racist violence inflicted upon her son and set the Civil Rights Movement into motion.</em></p>
<p><em>Day 3:<br>Gulf Islands National Seashore is the perfect place to visit today. Millions of visitors are drawn to the Gulf of Mexico for Gulf Islands National Seashore's emerald coast waters, magnificent white beaches, fertile marshes and historical landscapes. Come explore with us today!</em></p>
<p><em>Day 4:<br>Medgar and Myrlie Evers Home National Monument is next on the list. Medgar and Myrlie Evers were partners in the civil rights struggle. The assassination of Medgar Evers in the carport of their home on June 12, 1963, was the first murder of a nationally significant leader of the American Civil Rights Movement, and it became a catalyst for passage of the Civil Rights Act of 1964. Myrlie Evers continued to promote issues of racial equality and social justice.</em></p>
<p><em>Day 5:<br>Natchez National Historical Park is a great place to visit today. Discover the history of all the peoples of Natchez, Mississippi, from European settlement, African enslavement, the American cotton economy, to the Civil Rights struggle on the lower Mississippi River.</em></p>
<p><em>Day 6:<br>Today, explore the Natchez Trace National Scenic Trail. The Natchez Trace National Scenic Trail is five sections of hiking trail running roughly parallel to the 444-mile long Natchez Trace Parkway scenic motor road. The foot trails total more than 60 miles and offer opportunities to explore wetlands, swamps, hardwood forest, and the history of the area. For What's Open What's Close visit www.nps.gov/natr/planyourvisit/what-is-open-what-is-closed.htm</em></p>
<p><em>Day 7:<br>The Natchez Trace Parkway is the perfect place to visit today. The Natchez Trace Parkway is a 444-mile recreational road and scenic drive through three states. It roughly follows the "Old Natchez Trace" a historic travel corridor used by American Indians, "Kaintucks," European settlers, slave traders, soldiers, and future presidents. Today, people can enjoy not only a scenic drive but also hiking, biking, horseback riding, and camping along the Parkway.</em></p>
<p><em>Day 8:<br>Finish your trip by exploring Shiloh National Military Park. Visit the sites of the most epic struggle in the Western Theater of the Civil War. Nearly 110,000 American troops clashed in a bloody contest that resulted in 23,746 casualties; more casualties than in all of America's previous wars combined. Explore both the Shiloh and Corinth battlefields to discover the impact of this struggle on the soldiers and on the nation.</em></p>
<p>I then decided to try it with the Claude Instant models to see how well it did. To do this you do have to change the <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html#model-parameters-claude">model parameters passed in,</a> but it isn't too different. </p>
<p>As you can see, this model does about as well as the <code>Jurassic-2 Ultra</code> model, but looking back at the pricing table, it's 10x cheaper for the input tokens and 60% of the price for output tokens. </p>
<p><em>Based on the descriptions provided, here is an 8-day itinerary focusing on activities in the national parks:</em></p>
<p><em>Day 1: Spend the morning at Brices Cross Roads National Battlefield Site learning about the Confederate victory there through wayside exhibits and a short film. In the afternoon, visit Emmett Till and Mamie Till-Mobley National Monument to learn about their story and its impact on the civil rights movement.</em></p>
<p><em>Day 2: Dedicate the full day to exploring Gulf Islands National Seashore. Hike or bike along the beautiful coastal trails, beachcomb for seashells, and spot wildlife along the shoreline and in the marshes.</em></p>
<p><em>Day 3: Spend the morning touring the preserved Medgar and Myrlie Evers Home National Monument and learning about their civil rights activism. In the afternoon, visit Natchez National Historical Park to discover the diverse history of the Natchez area through historic buildings, walking trails, and exhibits.</em></p>
<p><em>Day 4: Enjoy a scenic drive along the Natchez Trace Parkway, stopping at various overlooks for photo opportunities. Consider hiking or biking portions of the adjacent Natchez Trace National Scenic Trail as well for more active exploration.</em></p>
<p><em>Day 5: Immerse yourself in the epic Civil War history of Shiloh National Military Park by touring the battlefield sites and learning about the bloody two-day battle that took place there.</em></p>
<p><em>Day 6: Spend the morning at Tupelo National Battlefield learning about the 1864 battle that took place in the city. In the afternoon, continue on to Vicksburg National Military Park for an overview film and to tour the extensive battlefields and fortifications.</em></p>
<p><em>Day 7: Dedicate another full day to further exploring Vicksburg NMP, such as taking a ranger-led tour or hiking additional trails to discover the nuances of the long siege that took place here.</em></p>
<p><em>Day 8: Before departing, consider returning to a favorite park from earlier in the week for additional exploration or to see anything you may have missed initially.</em></p>
<p>I also tested the Claude v2, which is more advanced but also costs about the same as the Jurassic-2 Ultra model. This didn't make a noticeably better response, so for this use case I would definitely stick with the Anthropic Claude Instant model.</p>
<h2 id="heading-how-to-take-it-to-the-next-level">How to Take it to the Next Level</h2>
<p>That’s a really good start for such a simple prompt. You could iterate on it, improving it and testing different styles and wording. You could try and find other sources of information to pass into your prompt, as giving it useful information to use in the response is often the best way to improve the results of these models.</p>
<h2 id="heading-how-to-use-this-process-in-other-apps">How to Use this Process in Other Apps</h2>
<p>Through this process, you've learnt to build an app that leverages the power of AI. You can now follow this same process to add AI power to your own AWS apps.</p>
<ul>
<li>Find a use case where AI could generate you some content</li>
<li>Gather some data that will help the AI create a better response.</li>
<li>Generate the prompt</li>
<li>Call the <code>InvokeModel</code> function in Bedrock</li>
<li>Deploy and Test your AI function</li>
<li>Change the Prompt and Parameters to see what results in the best responses</li>
</ul>
<h2 id="heading-how-to-learn-more-about-serverless">How to Learn More about Serverless</h2>
<p>Now that you know how to build AI into your apps, you probably have loads of app ideas. </p>
<p>If you want to learn how to build the rest of that idea then check out my <a target="_blank" href="https://completecoding.io/the-ultimate-guide-to-backend-serverless-development/">ultimate guide to Serverless</a> or my course which helps you <a target="_blank" href="https://serverlessmasterclass.com/7-serverless-projects?utm_source=freecodecamp&amp;utm_medium=text">Master Serverless by building 7 real world projects</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Next.js App with Custom Domain on AWS Using SST ]]>
                </title>
                <description>
                    <![CDATA[ Serverless architectures have transformed the way we build and deploy applications in the cloud, bringing in more efficiency and scalability. In this article, we'll dive into the Serverless Stack Toolkit (SST), a framework for building serverless app... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-a-next-js-app-with-custom-domain-on-aws-using-sst/</link>
                <guid isPermaLink="false">66ba10bc90067134b63982c3</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Arunachalam B ]]>
                </dc:creator>
                <pubDate>Mon, 24 Jul 2023 07:18:19 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/AWS---SST---Banner.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Serverless architectures have transformed the way we build and deploy applications in the cloud, bringing in more efficiency and scalability.</p>
<p>In this article, we'll dive into the Serverless Stack Toolkit (SST), a framework for building serverless applications. We'll deploy a Next.js application and set up a custom domain, all without visiting the AWS console. </p>
<p>Let's begin this journey!</p>
<h2 id="heading-what-does-serverless-mean">What Does Serverless Mean?</h2>
<p>The term "serverless" refers to a cloud computing model where developers can build and deploy applications without the need to manage servers. In a serverless architecture, the cloud provider handles server provisioning, scaling, and maintenance. This allows developers to focus solely on writing code for their applications.</p>
<p>With serverless, developers are billed based on actual usage rather than fixed server costs, making it a cost-effective and scalable solution. It offers increased flexibility and agility, as resources are automatically allocated and released based on demand. This eliminates the need for developers to worry about infrastructure management.</p>
<p>Now that we have a good idea of what serverless means, let's see what the Serverless Stack Toolkit (SST) is.</p>
<h2 id="heading-understanding-serverless-stack-toolkit-sst">Understanding Serverless Stack Toolkit (SST)</h2>
<p>The Serverless Stack Toolkit, or SST in short, is a flexible, open-source framework designed to enable faster development and reliable deployment of serverless applications on AWS.</p>
<p>It aims to make it easier for developers to define their application's infrastructure using AWS CDK (Cloud Development Kit).</p>
<p>You can use it to test applications in real-time with Live Lambda Development, debug code in Visual Studio Code, manage applications through a web-based dashboard, and deploy to multiple environments and regions seamlessly.</p>
<h2 id="heading-benefits-of-using-sst">Benefits of Using SST</h2>
<p>Here are some benefits of using the SST stack:</p>
<h3 id="heading-infrastructure-as-code">Infrastructure as Code</h3>
<p>With SST, developers can define their application's infrastructure programmatically using AWS CDK. This improves version control and collaboration among team members.</p>
<h3 id="heading-efficient-testing-and-debugging">Efficient Testing and Debugging</h3>
<p>SST enables live Lambda development, making it easier to test and debug serverless applications locally before deployment to AWS. This reduces potential issues and ensures smoother deployment.</p>
<h3 id="heading-simplified-deployment">Simplified Deployment</h3>
<p>SST simplifies the deployment process, allowing developers to deploy applications to multiple environments and regions effortlessly.</p>
<h3 id="heading-language-flexibility">Language Flexibility</h3>
<p>SST supports multiple programming languages, including JavaScript, TypeScript, Go, Python, C#, and F#, providing developers with the flexibility to use their preferred language for building serverless applications.</p>
<p>Now that we have understood what SST is and some of its benefits, let's see the power of SST in action.</p>
<h2 id="heading-how-to-configure-aws">How to Configure AWS</h2>
<p>Before we add SST we have to configure some AWS credentials. To do that, type the below command in your terminal:</p>
<pre><code>aws configure
</code></pre><p><img src="https://www.freecodecamp.org/news/content/images/2023/07/Screenshot-from-2023-07-18-02-29-05.png" alt="Image" width="600" height="400" loading="lazy">
<em>AWS Configure</em></p>
<p>You'll be required to enter your AWS Access Key ID, Secret Access Key, Region name and output format. If you don't have these keys, please <a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html">create an IAM user</a> and enter the credentials.</p>
<h2 id="heading-how-to-add-sst-to-your-nextjs-app">How to Add SST to Your Next.js App</h2>
<p>We can use SST in an existing Next.js app in <em><a target="_blank" href="https://docs.sst.dev/what-is-sst#drop-in-mode">drop-in mode</a></em> or inside a monorepo app in <em><a target="_blank" href="https://docs.sst.dev/what-is-sst#standalone-mode">standalone mode</a></em>.</p>
<p>In this article, we'll create new Next.js project and add SST which follows drop-in mode installation using the commands below:</p>
<pre><code>yarn create next-app
cd my-app
yarn create sst
yarn install
</code></pre><p><strong>Note</strong>: You should ensure that you have the <code>index.tsx</code> file inside the <code>/pages</code> folder. Without the file, you'll get errors while deploying your app using SST. You don't need to make any changes to this file.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-100.png" alt="Image" width="600" height="400" loading="lazy">
<em>Folder structure</em></p>
<p>Once you run the above commands, SST will create two new files —<code>sst.config.ts</code> and <code>sst-env.d.ts</code></p>
<p>We have to define all our infrastructure and stacks in the <code>sst.config.ts</code> file.</p>
<p>You can use these commands to run the app locally:</p>
<pre><code># Start SST locally
yarn sst dev

# Start Next.js locally
yarn dev
</code></pre><p>On executing the <code>yarn sst dev</code> command, you'll be asked to enter the stage name. Please enter your environment name. I'll use <code>dev</code> for this project's stage name.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-96.png" alt="Image" width="600" height="400" loading="lazy">
<em>Start SST locally</em></p>
<p>Just sit back and watch. It will automatically create the necessary IAM roles, permissions and CloudFormation stacks.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-97.png" alt="Image" width="600" height="400" loading="lazy">
<em>SST - Creating the neccessary IAM roles, permissions and stack</em></p>
<p>Notice in the image above that you can see the Console URL, <code>https://console.sst.dev/sst-demo/dev</code>. With the Console URL, you can view real-time logs, invoke functions, replay invocations, make queries, run migrations, view uploaded files, query your GraphQL APIs, and more!</p>
<p>Just awesome right? I would recommend you to visit the official <a target="_blank" href="https://docs.sst.dev/console">documentation</a> to learn more about the services they offer.</p>
<p>Next, start the Next.js site by running <code>yarn dev</code>. You should see the default page after that.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-98.png" alt="Image" width="600" height="400" loading="lazy">
<em>Next.js default page</em></p>
<p>Our Next.js app is now ready to be deployed to AWS! Just run the following command and see the magic.</p>
<pre><code>yarn sst deploy --stage prod
</code></pre><p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-99.png" alt="Image" width="600" height="400" loading="lazy">
<em>OpenNext building the Next.js App</em></p>
<p>It will automatically start building the app using <a target="_blank" href="https://open-next.js.org/">OpenNext</a> , deploy it to AWS using <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">CDK</a>, and output the CloudFront URL. Click on the link and you should be able to see your app up and running.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-102.png" alt="Image" width="600" height="400" loading="lazy">
<em>SST - Deployed changes and outputs CloudFront url</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-101.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Next.js app up and running</em></p>
<h2 id="heading-how-to-create-infrastructure-using-sst">How to Create Infrastructure using SST</h2>
<p>To create an infrastructure, we simply need to edit <code>sst.config.ts</code> and import any AWS services like S3 bucket, RDS, API Gateway, and so on from <code>sst/constructs</code></p>
<p>Let's add a simple S3 file upload feature. Open <code>sst.config.ts</code> file and add the code below:</p>
<pre><code><span class="hljs-keyword">import</span> { SSTConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst"</span>;
<span class="hljs-keyword">import</span> {Bucket, NextjsSite } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst/constructs"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  config(_input) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"sst-tutorial"</span>,
      <span class="hljs-attr">region</span>: <span class="hljs-string">"us-east-1"</span>,
    };
  },
  stacks(app) {
    app.stack(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Site</span>(<span class="hljs-params">{ stack }</span>) </span>{
      <span class="hljs-keyword">const</span> bucket = <span class="hljs-keyword">new</span> Bucket(stack, <span class="hljs-string">"public"</span>);
      <span class="hljs-keyword">const</span> site = <span class="hljs-keyword">new</span> NextjsSite(stack, <span class="hljs-string">"site"</span>,{
        <span class="hljs-attr">bind</span>:[bucket],
      });
      stack.addOutputs({
        <span class="hljs-attr">SiteUrl</span>: site.url,
      });
    });
  },
} satisfies SSTConfig;
</code></pre><p>Here, we're creating a new public S3 bucket and binding it with our <code>NextjsSite</code>. </p>
<p>Let's edit our index page to add file upload feature.</p>
<h3 id="heading-how-to-upload-files-to-s3-using-sst">How to Upload Files to S3 using SST</h3>
<p>To upload a file to S3, we need to generate a pre-signed URL. To do that, we need to add this package <code>@aws-sdk/s3-request-presigner</code> in our repo.</p>
<pre><code>yarn add @aws-sdk/s3-request-presigner
</code></pre><p>Open <code>index.tsx</code> file and create a function called <code>getServerSideProps</code> above the Home function, as shown in the below code snippet.</p>
<pre><code>...
import { Bucket } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst/node/bucket"</span>;
<span class="hljs-keyword">import</span> { getSignedUrl } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/s3-request-presigner"</span>;
<span class="hljs-keyword">import</span> { S3Client, PutObjectCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-s3"</span>;
...
export <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getServerSideProps</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> PutObjectCommand({
    <span class="hljs-attr">ACL</span>: <span class="hljs-string">"public-read"</span>,
    <span class="hljs-attr">Key</span>: crypto.randomUUID(),
    <span class="hljs-attr">Bucket</span>: Bucket.public.bucketName,
  });
  <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">await</span> getSignedUrl(<span class="hljs-keyword">new</span> S3Client({}), command);
  <span class="hljs-keyword">const</span> bucketName = Bucket.public.bucketName
  <span class="hljs-built_in">console</span>.log(bucketName)
  <span class="hljs-keyword">return</span> { <span class="hljs-attr">props</span>: { url } };
}
</code></pre><p>Update the <code>Home()</code> function with the following code.</p>
<pre><code><span class="hljs-keyword">import</span> styles <span class="hljs-keyword">from</span> <span class="hljs-string">"@/styles/Home.module.css"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params">{ url }: { url: string }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{styles.main}</span>&gt;</span>
     <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{styles.center}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
          <span class="hljs-attr">href</span>=<span class="hljs-string">"https://5minslearn.gogosoon.com/?ref=github_sst_app"</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">{styles.card}</span>
          <span class="hljs-attr">target</span>=<span class="hljs-string">"_blank"</span>
          <span class="hljs-attr">rel</span>=<span class="hljs-string">"noopener noreferrer"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{inter.className}</span>&gt;</span>
            5minslearn <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>-<span class="hljs-symbol">&amp;gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{inter.className}</span>&gt;</span>Learn tech in 5mins<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">form</span>
        <span class="hljs-attr">className</span>=<span class="hljs-string">{styles.form}</span>
        <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{async</span> (<span class="hljs-attr">e</span>) =&gt;</span> {
          e.preventDefault();

          const file = (e.target as HTMLFormElement).file.files?.[0]!;

          const image = await fetch(url, {
            body: file,
            method: "PUT",
            headers: {
              "Content-Type": file.type,
              "Content-Disposition": `attachment; filename="${file.name}"`,
            },
          });

          window.location.href = image.url.split("?")[0];
        }}
      &gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">accept</span>=<span class="hljs-string">"image/png, image/jpeg"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{inter.className}</span>&gt;</span>
          Upload
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre><p>I added an input with a file <code>type</code> and a button for submitting the form. The selected image will be uploaded to S3 when the form is submitted. It's time to deploy the changes.</p>
<p>To deploy the changes, run the <code>yarn sst deploy</code> command.</p>
<p>Once deployed you'll see a page like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-103.png" alt="Image" width="600" height="400" loading="lazy">
<em>Next.js up and running with updated changes</em></p>
<p>Now you can upload any image and check your S3. The selected file will be uploaded to your S3 bucket.</p>
<p>Great, we have successfully deployed the changes. But we still have the random URL generated by CloudFront which may be difficult to memorize for humans. Let's configure a custom domain.</p>
<h2 id="heading-how-to-configure-custom-domains">How to Configure Custom Domains</h2>
<p>To configure custom domains, we need a valid domain or sub-domain. You can create one using Route 53 or your preferred domain provider like GoDaddy, Namecheap, and so on.</p>
<p>If you have a domain on an external DNS provider, you'll need to create an SSL certificate on AWS Certificate Manager (ACM).</p>
<p>I have my domain on Cloudflare. If you have yours with other providers like Namecheap or GoDaddy, then the steps below should still work for you. </p>
<h3 id="heading-how-to-point-cname-to-cloudfront">How to Point CNAME to CloudFront</h3>
<ol>
<li>Login into your DNS provider.</li>
<li>Add a CNAME. In my case, I used <code>aws</code> as the name because my domain is <code>aws.gogosoon.com</code>, and target as the CloudFront URL without <code>https</code>.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-104.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We've successfully pointed our CNAME to CloudFront. Now let's create an SSL certificate for our domain.</p>
<h3 id="heading-how-to-create-acm-certificate">How to Create ACM Certificate</h3>
<p>ACM certificates are managed SSL/TLS certificates that can be used with a variety of AWS services, including CloudFront.</p>
<p>However, there is a specific requirement for using ACM certificates with CloudFront: the certificate must be created in the <strong>US East (N. Virginia) region (us-east-1)</strong>. The reason for this is that CloudFront has all of its provisioning/administrative infrastructure based in <strong>us-east-1</strong>. </p>
<p>Quoting from their <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html#https-requirements-aws-region">documentation</a>:</p>
<blockquote>
<p>To use a certificate in AWS Certificate Manager (ACM) to require HTTPS between viewers and CloudFront, make sure you request (or import) the certificate in the US East (N. Virginia) Region (us-east-1).</p>
</blockquote>
<p>Here are the steps to follow to create an ACM:</p>
<ol>
<li>Login into AWS console.</li>
<li>Search for certificate manager, switch to <strong>us-east-1</strong> and click on "Request Certificate" in the sidebar.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-105.png" alt="Image" width="600" height="400" loading="lazy">
<em>AWS ACM - Request Certificate</em></p>
<ol start="3">
<li>Enter the domain name you pointed to in your DNS provider configuration. Under "Validation method", select "Email validation" and click next.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-114.png" alt="Image" width="600" height="400" loading="lazy">
<em>AWS ACM - Request Certificate</em></p>
<ol start="4">
<li>A certificate with the status of "Pending Validation" will be created. You'll receive an email from AWS with a link to validate the request.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-106.png" alt="Image" width="600" height="400" loading="lazy">
<em>ACM certificate with pending status</em></p>
<ol start="5">
<li>Once you click on the link in the email, the status of the certificate will be changed to "Issued". Copy the ARN – we'll need it in the next steps.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-111.png" alt="Image" width="600" height="400" loading="lazy">
<em>AWS ACM certificate issued</em></p>
<p>Now that we've created the certificate successfully, let's create the alternate domain for CloudFront.</p>
<h3 id="heading-how-to-create-an-alternate-domain-for-cloudfront-distribution">How to Create an Alternate Domain for CloudFront Distribution</h3>
<ol>
<li>Log into the AWS Console and search for CloudFront.</li>
<li>Click on the distribution created by SST.</li>
<li>In the "General" tab, click the "Edit" button.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-115.png" alt="Image" width="600" height="400" loading="lazy">
<em>Edit CloudFront distribution</em></p>
<ol start="4">
<li>Enter the alternate domain name and select the certificate that we created. Leave all other options as default and click on the "Save changes" button.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-108.png" alt="Image" width="600" height="400" loading="lazy">
<em>Add alternate domain for CloudFront distribution</em></p>
<p>All set! Let's edit our app to deploy the changes to our custom domain.</p>
<h3 id="heading-how-to-configure-external-custom-domain-using-sst">How to Configure External Custom Domain using SST</h3>
<p>Update the <code>sst.config.ts</code> file with the following code. Paste the ARN you copied while creating the certificate as a value for the variable <code>certArn</code>. Replace the <code>domainName</code> with your domain:</p>
<pre><code><span class="hljs-keyword">import</span> { SSTConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst"</span>;
<span class="hljs-keyword">import</span> {Bucket, NextjsSite } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst/constructs"</span>;
<span class="hljs-keyword">import</span> { Certificate } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-certificatemanager"</span>;


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  config(_input) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">name</span>: <span class="hljs-string">"sst-tutorial"</span>,
      <span class="hljs-attr">region</span>: <span class="hljs-string">"us-east-1"</span>,
    };
  },
  stacks(app) {
    app.stack(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Site</span>(<span class="hljs-params">{ stack }</span>) </span>{
      <span class="hljs-keyword">const</span> bucket = <span class="hljs-keyword">new</span> Bucket(stack, <span class="hljs-string">"public"</span>);
      <span class="hljs-keyword">const</span> certArn = <span class="hljs-string">'Paste the certificate arn'</span>
      <span class="hljs-keyword">const</span> site = <span class="hljs-keyword">new</span> NextjsSite(stack, <span class="hljs-string">"site"</span>,{
        <span class="hljs-attr">bind</span>:[bucket],
        <span class="hljs-attr">customDomain</span>: {
          <span class="hljs-attr">isExternalDomain</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">domainName</span>: <span class="hljs-string">"aws.gogosoon.com"</span>,
          <span class="hljs-attr">cdk</span>: {
            <span class="hljs-attr">certificate</span>: Certificate.fromCertificateArn(stack, <span class="hljs-string">"MyCert"</span>, certArn),
          },
        },
      });
      stack.addOutputs({
        <span class="hljs-attr">SiteUrl</span>: site.customDomainUrl || site.url,
      });
    });
  },
} satisfies SSTConfig;
</code></pre><p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-110.png" alt="Image" width="600" height="400" loading="lazy">
<em>sst.config.ts - File changes</em></p>
<p>Run <code>yarn sst deploy</code> to deploy the changes to a custom domain. Once deployed, you should have the app running on the custom URL. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-113.png" alt="Image" width="600" height="400" loading="lazy">
<em>Next.js deployed with custom domain using SST</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-112.png" alt="Image" width="600" height="400" loading="lazy">
<em>Next.js app up and running with custom domain</em></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Voila! Our Next.js app is now deployed to AWS, and we've connected it with our custom domain. Please check out the source code <a target="_blank" href="https://github.com/5minslearn/sst-demo">here</a>.</p>
<p>The SST framework provides an excellent toolset for deploying serverless applications, contributing significantly to development speed, scalability, and error handling.</p>
<p>Feel free to explore more about <a target="_blank" href="https://docs.sst.dev/">SST</a> and its potential in transforming your cloud development experience. Happy coding!</p>
<p>If you wish to learn more about AWS Services, subscribe to my <a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_aws_sst_nextjs_deploy">email newsletter</a> (<a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_aws_sst_nextjs_deploy">https://5minslearn.gogosoon.com/</a>) and follow me on social media.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Offer Custom APIs to Your Users with AWS API Gateway ]]>
                </title>
                <description>
                    <![CDATA[ In the world of cloud computing and serverless architecture, AWS API Gateway is a powerful tool that helps you build robust, secure, and scalable APIs.  In this tutorial, I'll introduce you to API Gateway and explain the benefits of using this helpfu... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-offer-custom-apis-to-your-users-aws-api-gateway/</link>
                <guid isPermaLink="false">66ba10cda1a94b68c6ae94b2</guid>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Arunachalam B ]]>
                </dc:creator>
                <pubDate>Wed, 21 Jun 2023 13:56:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/06/AWS-API-Gateway-Banner.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In the world of cloud computing and serverless architecture, AWS API Gateway is a powerful tool that helps you build robust, secure, and scalable APIs. </p>
<p>In this tutorial, I'll introduce you to API Gateway and explain the benefits of using this helpful tool. Then I'll show you how to create and deploy a Rest API, and create usage plans to offer API keys. Alright, let's get started. </p>
<h2 id="heading-what-is-api-gateway">What is API Gateway?</h2>
<p>AWS API Gateway is a fully managed service provided by Amazon Web Services (AWS) that simplifies the creation, deployment, and management of APIs at any scale. </p>
<p>It acts as a front door for applications, and allows you to create APIs that act as bridges between clients and back-end services. This enables secure and efficient communication.</p>
<h2 id="heading-why-do-you-need-api-gateway">Why Do You Need API Gateway?</h2>
<p>AWS API Gateway offers many benefits for businesses and developers. Here are a few benefits of using API Gateway. </p>
<h3 id="heading-scalability-and-high-availability">Scalability and High Availability</h3>
<p>With AWS API Gateway, scaling your APIs becomes much easier. It seamlessly handles traffic spikes by automatically scaling the underlying infrastructure. This results in high availability and helps prevent service disruptions.</p>
<h3 id="heading-security-and-authentication">Security and Authentication</h3>
<p>API Gateway offers robust security features, including built-in authentication and authorization mechanisms. </p>
<p>It supports User Authentication through IAM Roles for internal applications, Cognito for external applications (for example Mobile users), and it also supports custom authorizers. </p>
<h3 id="heading-integration-with-other-aws-services">Integration with other AWS Services</h3>
<p>As part of the AWS ecosystem, API Gateway seamlessly integrates with a range of other AWS services. This enables you to leverage additional functionalities like AWS Lambda functions, AWS Cognito for user management, and AWS CloudWatch for monitoring and logging.</p>
<h3 id="heading-api-lifecycle-management">API Lifecycle Management</h3>
<p>With API Gateway, you can easily version, deploy, and manage different stages of your APIs. This simplifies the process of rolling out updates, testing new features, and managing different environments such as development, staging, and production.</p>
<p>I hope by now you understood what API Gateway is and why it's valuable. Let's dive into creating our very own API Gateway.</p>
<h2 id="heading-how-to-create-an-aws-api-gateway">How to Create an AWS API Gateway</h2>
<p>In this section, we will:</p>
<ul>
<li>Create a Rest API with the GET method</li>
<li>Integrate it with a simple hello world lambda function and deploy it</li>
</ul>
<p>Let's begin with creating a lambda function</p>
<h2 id="heading-how-to-create-an-aws-lambda-function">How to Create an AWS Lambda Function</h2>
<p>Log in to the AWS Management <a target="_blank" href="https://console.aws.amazon.com/">Console</a> and search for "Lambda" in the AWS Management Console search bar. Click on Create Function. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-145.png" alt="Image" width="600" height="400" loading="lazy">
<em>Navigate to AWS Lambda Console</em></p>
<p>Select the "Author from scratch" option, enter a name for your lambda function, select the "Python" runtime, and click the Create Function button at the bottom right. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-146.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a AWS Lambda Function</em></p>
<p>Once the function is created, update the following code and deploy the changes:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> json

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lambda_handler</span>(<span class="hljs-params">event, context</span>):</span>
    body = <span class="hljs-string">"Hello from 5minslearn!"</span>
    statusCode = <span class="hljs-number">200</span>
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"statusCode"</span>: statusCode,
        <span class="hljs-string">"body"</span>: json.dumps(body),
        <span class="hljs-string">"headers"</span>: {
            <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
        }
    }
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-147.png" alt="Image" width="600" height="400" loading="lazy">
<em>Deploy a Lambda Function</em></p>
<p>Congratulations! You have successfully created an AWS Lambda function. Now let's create the Rest API. </p>
<h2 id="heading-how-to-create-a-rest-api-and-integrate-it-with-aws-lambda">How to Create a Rest API and Integrate it with AWS Lambda</h2>
<p>Search for API Gateway in the search bar. In the REST API section, click on the Build button. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-183.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a Rest API</em></p>
<p>Choose the Protocol as Rest and select New API in the Create new API section. In the settings section enter the API name of your choice and leave Endpoint Type as the default. Then click the Create API button. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-148.png" alt="Image" width="600" height="400" loading="lazy">
<em>Configure creating a Rest API</em></p>
<p>Click the Actions Button on the top left. Next, Click Method and select the method as GET and click the Tick icon. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-149.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a Method</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-150.png" alt="Image" width="600" height="400" loading="lazy">
<em>Choose "GET" method</em></p>
<p>Select Lambda Function as the Integration type and enter the name of the Lambda function you created previously. Then save the function. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-151.png" alt="Image" width="600" height="400" loading="lazy">
<em>Select Method configuration</em></p>
<p>Once you click save, "Add Permission to Lambda Function" will prompt for confirmation. This basically means that you're allowing the API Gateway to invoke a Lambda function. In this case, it is "DemoFunction" Lambda function. Accept the confirmation and proceed to the next step. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-152.png" alt="Image" width="600" height="400" loading="lazy">
<em>Allow Permission to invoke Lambda Function from API Gateway</em></p>
<p>Click on Test. It will take you to a new page. Click on the "Test" button. You'll be able to see the response from the Lambda function on the right side panel. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-153.png" alt="Image" width="600" height="400" loading="lazy">
<em>Our API Architecture</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-184.png" alt="Image" width="600" height="400" loading="lazy">
<em>Test our API Gateway</em></p>
<p>As you have successfully tested your API, you're ready to deploy the API. To deploy the API, click on the Actions button once again and click on Deploy API. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-185.png" alt="Image" width="600" height="400" loading="lazy">
<em>Deploy the API Gateway</em></p>
<p>The Deploy API dialogue will popup. Select New Stage for Deployment stage and name it whatever you want. Click the Deploy button. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-155.png" alt="Image" width="600" height="400" loading="lazy">
<em>Configure API Gateway deployment</em></p>
<p>Click on Invoke URL shown at the top. You can see the response from the Lambda function. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-156.png" alt="Image" width="600" height="400" loading="lazy">
<em>API Gateway Created</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-186.png" alt="Image" width="600" height="400" loading="lazy">
<em>Test our API</em></p>
<p>Great! We successfully created the Rest API, integrated it with the Lambda function, and deployed it.</p>
<p>But you can do this with multiple services available on the market. Why would you choose AWS API Gateway? </p>
<p>Well. That's a interesting question. First of all, you can configure the usage plan for your API. The best part is you don't have to write any code for it. </p>
<p>Now let's create a Usage Plan, generate an API key, and make our Rest API accessible only by passing the API key in the Header. </p>
<h2 id="heading-how-to-create-an-api-gateway-usage-plan">How to Create an API Gateway Usage Plan</h2>
<p>In the left side bar click on Usage Plans and click the Create button. Enter the Name of your plan – I chose "Basic". Enter the Throttling and Quota sections as per your requirements and click Next. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-159.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create AWS API Gateway usage plan</em></p>
<p>Click on the Add API Stage button. Select the API and its stage. Click on the tick icon at right corner and select Next. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/Screenshot-from-2023-06-19-10-46-19.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a Stage for our API</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-164.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a Stage for our API</em></p>
<p>Click on Create API Key and add to Usage Plan. A modal will pop up. Enter the Name for API Key. For the API key, I selected Auto Generate but if you want to give a custom key you can enter a custom key. Hit the Save button. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-160.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a API Key to access the service</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-161.png" alt="Image" width="600" height="400" loading="lazy">
<em>Configure the API Key</em></p>
<p>Select Resources from the Sidebar, click on the GET API you just created, and click the Method Request. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-162.png" alt="Image" width="600" height="400" loading="lazy">
<em>Select the method</em></p>
<p>In the Settings section, update the API Key Required field to true and click the Tick icon. Once updated, don't forget to deploy the changes by hitting the Action dropdown. Your changes will not be updated otherwise. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-187.png" alt="Image" width="600" height="400" loading="lazy">
<em>Enable API Key Required field</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-165.png" alt="Image" width="600" height="400" loading="lazy">
<em>Deploy the API</em></p>
<p>Hit the same URL now and see the magic. </p>
<p>Forbidden!</p>
<p>Because our API layer is protected now. You have to pass the API key in the header to access the data. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-163.png" alt="Image" width="600" height="400" loading="lazy">
<em>Forbidden access if no API Key is provided</em></p>
<p>Now Click on the Usage Plans from the Sidebar. Select your plan and navigate to the API Keys tab. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-166.png" alt="Image" width="600" height="400" loading="lazy">
<em>Access your API Key</em></p>
<p>Click on the API key you created in Step 3. Click Show. Copy the API key. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-188.png" alt="Image" width="600" height="400" loading="lazy">
<em>List of API Keys</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-167.png" alt="Image" width="600" height="400" loading="lazy">
<em>Reveal your API Key</em></p>
<p>You have to pass the API Key in the 'x-api-key' header. Let's switch to the terminal to test this out. </p>
<p>Verify your Rest API without passing the API key at first. Open the terminal, and enter the following curl command. You will once again see the forbidden message. </p>
<pre><code class="lang-bash">curl --location --request GET <span class="hljs-string">'[enter your invoke url]'</span>
--header <span class="hljs-string">'Content-Type: application/json</span>
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-189.png" alt="Image" width="600" height="400" loading="lazy">
<em>Forbidden access without API Key in Terminal</em></p>
<p>Now pass the API key this time. Run the following curl command:</p>
<pre><code class="lang-bash">curl --location --request GET <span class="hljs-string">'[your invoke url]'</span> \
--header <span class="hljs-string">'x-api-key: [your api key]'</span> \
--header <span class="hljs-string">'Content-Type: application/json'</span> \
--data-raw <span class="hljs-string">''</span>
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-190.png" alt="Image" width="600" height="400" loading="lazy">
<em>Data received on passing API Key in x-api-key Header</em></p>
<p>You can see the output of the Lambda function because you passed 'x-api-key' in the header. </p>
<p>Awesome! You have successfully created the Usage plan, generated the API key, and attached it to the Rest API method and verified the integration. </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you learned what AWS API gateway is and how to create Usage Plans for the Rest API. </p>
<p>If you wish to learn more about AWS Services, subscribe to my <a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_aws_api_gateway">email newsletter</a> (<a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_aws_api_gateway">https://5minslearn.gogosoon.com/</a>) and follow me on social media.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Serverless ChatGPT App in 10 Minutes ]]>
                </title>
                <description>
                    <![CDATA[ Since OpenAI released an official API for ChatGPT in March 2023, many developers and entrepreneurs are interested in integrating it into their own business operations. But some significant barriers remain that make it difficult for them to do this: ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-a-serverless-chatgpt-app/</link>
                <guid isPermaLink="false">66d4604433b83c4378a51806</guid>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #chatbots ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Michael Yuan ]]>
                </dc:creator>
                <pubDate>Mon, 20 Mar 2023 16:52:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/03/c0fd422e-f234-49e2-85a6-24f91b0b9991-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Since OpenAI <a target="_blank" href="https://openai.com/blog/introducing-chatgpt-and-whisper-apis">released an official API for ChatGPT</a> in March 2023, many developers and entrepreneurs are interested in integrating it into their own business operations.</p>
<p>But some significant barriers remain that make it difficult for them to do this:</p>
<ul>
<li><p>OpenAI provides <a target="_blank" href="https://platform.openai.com/docs/guides/chat">a simple stateless API</a> for ChatGPT. The developer needs to keep track of the history and context of each conversation in a cache or database managed by the application. The developer also needs to manage and safeguard the API keys. There is a lot of boilerplate code unrelated to the application’s business logic.</p>
</li>
<li><p>The “natural” UI for the ChatGPT API application is a threaded chat. But it is difficult to create a “chat UI” in a traditional web or app framework. In fact, the most commonly used chat UI already exists in messaging apps like Slack, Discord, and even forums (for example, GitHub Discussions). We need a simple way to connect ChatGPT API responses to an existing messaging service.</p>
</li>
</ul>
<p>In this article, I will show you how to create a serverless GitHub bot. The bot allows GitHub users to chat with ChatGPT and each other in GitHub Issues. You can <a target="_blank" href="https://github.com/second-state/chat-with-chatgpt/issues/new">try it by asking a question</a>, or <a target="_blank" href="https://github.com/second-state/chat-with-chatgpt/issues">joining another conversation thread</a> by leaving a comment. In other words, this project uses GitHub Issues’ threaded messages UI as its own chat UI.</p>
<p><img src="https://i.imgur.com/7eWhQ8I.png" alt="Image" width="1842" height="1862" loading="lazy"></p>
<p><em>Figure 1. Learning Rust with ChatGPT. see</em> <a target="_blank" href="https://github.com/second-state/chat-with-chatgpt/issues/31"><em>https://github.com/second-state/chat-with-chatgpt/issues/31</em></a></p>
<p>The bot is a serverless function written in Rust. Just fork the example, deploy your fork on <a target="_blank" href="https://www.freecodecamp.org/news/p/dfeeb7b1-d632-448e-97b3-9fcd7df30bce/flows.network">flows.network</a>, and configure it to interact with your own GitHub repos and OpenAI keys. You will have a fully functional GitHub bot in 10 minutes. There is no need to set up a web server, or a webhook for GitHub API, or a cache / database server.</p>
<h2 id="heading-how-to-fork-the-template-repo">How to Fork the Template Repo</h2>
<p>First, <a target="_blank" href="https://github.com/flows-network/chatgpt-github-app">fork this template repo from GitHub</a>.</p>
<p>The <a target="_blank" href="https://github.com/flows-network/chatgpt-github-app/blob/main/src/lib.rs"><code>src/lib.rs</code></a> file contains the bot application (also known as the flow function). The <code>run()</code> function is called upon starting up. It listens for <code>issue_comment</code> and <code>issues</code> events from the GitHub repo <a target="_blank" href="https://github.com/second-state/chat-with-chatgpt"><code>owner/repo</code></a>. Those events are emitted when a new issue comment or a new issue is created in the repo.</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[no_mangle]</span>
<span class="hljs-meta">#[tokio::main(flavor = <span class="hljs-meta-string">"current_thread"</span>)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">run</span></span>() {
    <span class="hljs-comment">// Setup variables for</span>
    <span class="hljs-comment">//   ower: GitHub org to install the bot</span>
    <span class="hljs-comment">//   repo: GitHub repo to install the bot</span>
    <span class="hljs-comment">//   openai_key_name: Name for your OpenAI API key</span>
    <span class="hljs-comment">// All the values can be set in the source code or as env vars</span>

    listen_to_event(&amp;owner, &amp;repo, <span class="hljs-built_in">vec!</span>[<span class="hljs-string">"issue_comment"</span>, <span class="hljs-string">"issues"</span>], |payload| {
        handler(&amp;owner, &amp;repo, &amp;openai_key_name, payload)
    })
    .<span class="hljs-keyword">await</span>;
}
</code></pre>
<p>The <code>handler()</code> function processes the events received by <code>listen_to_event()</code>. If the event is a new comment in an issue, the bot calls OpenAI's ChatGPT API to add the comment text into an existing conversation identified by the <code>issue.number</code>. It receives a response from ChatGPT, and adds a comment in the issue.</p>
<p>The flow function here automatically and transparently manages the conversation history with the ChatGPT API in a local storage. The OpenAI API key is also stored in the local storage so that instead of putting the secret text in the source code, the key can be identified by a string name in <code>openai_key_name</code>.</p>
<pre><code class="lang-rust">EventPayload::IssueCommentEvent(e) =&gt; {
    <span class="hljs-keyword">if</span> e.comment.user.r#<span class="hljs-class"><span class="hljs-keyword">type</span> != "<span class="hljs-title">Bot</span></span><span class="hljs-string">" {
        if let Some(b) = e.comment.body {
            if let Some(r) = chat_completion (
                    openai_key_name,
                    &amp;format!("</span>issue#{}<span class="hljs-string">", e.issue.number),
                    &amp;b,
                    &amp;ChatOptions::default(),
            ) {
                if let Err(e) = issues.create_comment(e.issue.number, r.choice).await {
                    write_error_log!(e.to_string());
                }
            }
        }
    }
}</span>
</code></pre>
<p>If the event is a new issue, the flow function creates a new conversation identified by <code>issue.number</code>, and requests a response from ChatGPT.</p>
<pre><code class="lang-rust">EventPayload::IssuesEvent(e) =&gt; {
    <span class="hljs-keyword">if</span> e.action == IssuesEventAction::Closed {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">let</span> title = e.issue.title;
    <span class="hljs-keyword">let</span> body = e.issue.body.unwrap_or(<span class="hljs-string">""</span>.to_string());
    <span class="hljs-keyword">let</span> q = title + <span class="hljs-string">"\n"</span> + &amp;body;
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(r) = chat_completion (
            openai_key_name,
            &amp;<span class="hljs-built_in">format!</span>(<span class="hljs-string">"issue#{}"</span>, e.issue.number),
            &amp;q,
            &amp;ChatOptions::default(),
    ) {
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Err</span>(e) = issues.create_comment(e.issue.number, r.choice).<span class="hljs-keyword">await</span> {
            write_error_log!(e.to_string());
        }
    }
}
</code></pre>
<h2 id="heading-how-to-deploy-the-serverless-flow-function">How to Deploy the Serverless Flow Function</h2>
<p>As we can see, the flow function code calls SDK APIs to perform complex operations. For example,</p>
<ul>
<li><p>The <code>listen_to_event()</code> function registers a webhook URL through GitHub API so that the <code>handler()</code> function will be called when certain events occur in GitHub.</p>
</li>
<li><p>The <code>chat_completion()</code> function calls the ChatGPT API with the named API key and past history / context of the specified conversation. The API key and conversation history are stored in a Redis cache.</p>
</li>
</ul>
<p>The webhook server and the Redis cache are both external services the SDK depends on. That means the flow function must run inside a managed host environment that provides such external services. <a target="_blank" href="https://flows.network/">Flows.network</a> is a PaaS (Platform as a Service) host for the flow function SDKs.</p>
<p>In order to deploy the flow function on flows.network, you simply need to import its source code to the PaaS.</p>
<p>First, sign into flows.network from your GitHub account. Import your forked GitHub repo that contains the flow function source code and choose "With Environment Variables".</p>
<p>Note that this is NOT the GitHub repo where you want to deploy the bot. This is the repo for your forked flow function source code.</p>
<p><img src="https://i.imgur.com/CH1nUf8.png" alt="Image" width="1362" height="1182" loading="lazy"></p>
<p><em>Figure 2. Import the GitHub repo you forked from the flow function template into flows.network.</em></p>
<p>Set the environment variables to point the flow function to the OpenAI API key name (<code>open_ai_key</code>) and GitHub repo (<code>owner</code> and <code>repo</code>).</p>
<p>The GitHub <code>owner</code> and <code>repo</code> variables here point to the GitHub repo where you want to deploy the bot, NOT the repo for the flow function source code.</p>
<p><img src="https://i.imgur.com/5gcTKMv.png" alt="Image" width="726" height="527" loading="lazy"></p>
<p><em>Figure 3. Set the environment variables for the GitHub repo where you want to deploy the bot, as well as the OpenAI API key name.</em></p>
<p>Flows.network will fetch the source code and build the Rust source code into Wasm bytecode using the standard <code>cargo</code> toolchain. It will then run the Wasm flow function in the <a target="_blank" href="https://github.com/WasmEdge/WasmEdge">WasmEdge Runtime</a>.</p>
<h2 id="heading-how-to-connect-the-flow-function-to-github-and-openai">How to Connect the Flow Function to GitHub and OpenAI</h2>
<p>While the flow function requires connections to the OpenAI and GitHub APIs, the source code has no hardcoded API keys, access tokens, or OAUTH logic. The flows function SDKs have made it easy and safe for developers to interact with external SaaS API services.</p>
<p>Flows.network discovers that the flow function requires connections to the OpenAI and GitHub APIs. It presents UI workflows for the developers to:</p>
<ul>
<li><p>Log into GitHub, authorize access to events, and register the flow function as the webhook for receiving those events.</p>
</li>
<li><p>Associate an OpenAI API key with the name <code>openai_key_name</code>.</p>
</li>
</ul>
<p><img src="https://i.imgur.com/CpLDrub.png" alt="Image" width="1393" height="484" loading="lazy"></p>
<p><em>Figure 4. The external services required by the flow function are connected and turned green.</em></p>
<p>Once the external SaaS APIs are connected and authorized, they turn green on the flow function dashboard. The flow function will now receive the events it <code>listen_to_event()</code> for. It will also get transparent access to Redis for the named OpenAI API key and the cached conversation context to support the <code>chat_completion()</code> SDK function.</p>
<h2 id="heading-whats-next">What's next</h2>
<p>The GitHub bot is just one of many bot types the flows.network can support. By connecting the flow function to a Slack channel, you can get ChatGPT to participate in your group discussion. Here is an example of a Slack-based ChatGPT bot.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/flows-network/collaborative-chat">https://github.com/flows-network/collaborative-chat</a></div>
<p> </p>
<p><img src="https://i.imgur.com/voB27bj.png" alt="Image" width="1446" height="842" loading="lazy"></p>
<p><em>Figure 5. The Slack ChatGPT bot.</em></p>
<p>Another example is to have ChatGPT answering legal questions in a Slack channel. The flow function prepends the legal question with a prompt.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/flows-network/robo-lawyer">https://github.com/flows-network/robo-lawyer</a></div>
<p> </p>
<p><img src="https://i.imgur.com/afDM5im.png" alt="Image" width="1544" height="978" loading="lazy"></p>
<p><em>Figure 6. The Slack robo lawyer bot.</em></p>
<p>Besides GitHub and Slack, there are many SaaS products you can integrate into flows.network through their APIs.</p>
<p>While the example flow functions are written in Rust, we aim to support JavaScript-based flow function SDKs. In another word, platform SDK functions such as <code>listen_to_event()</code> and <code>chat_completion()</code> will have a JavaScript version. The JavaScript flow function runs inside the <a target="_blank" href="https://github.com/WasmEdge/WasmEdge">WasmEdge Runtime</a> on the flows.network platform through the <a target="_blank" href="https://wasmedge.org/docs/develop/javascript/intro">WasmEdge-QuickJS</a> module.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
