<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oladele Omoarukhe</title>
    <description>The latest articles on DEV Community by Oladele Omoarukhe (@oladele_omoarukhe_9c700bf).</description>
    <link>https://dev.to/oladele_omoarukhe_9c700bf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2648413%2F6590a2eb-6cc5-4a44-b3ee-cfff0aeed280.jpg</url>
      <title>DEV Community: Oladele Omoarukhe</title>
      <link>https://dev.to/oladele_omoarukhe_9c700bf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oladele_omoarukhe_9c700bf"/>
    <language>en</language>
    <item>
      <title>Migrating a TypeScript Serverless API from Serverless Framework to Terraform with Datadog Observability</title>
      <dc:creator>Oladele Omoarukhe</dc:creator>
      <pubDate>Wed, 15 Jul 2026 03:33:09 +0000</pubDate>
      <link>https://dev.to/oladele_omoarukhe_9c700bf/migrating-a-typescript-serverless-api-from-serverless-framework-to-terraform-with-datadog-415i</link>
      <guid>https://dev.to/oladele_omoarukhe_9c700bf/migrating-a-typescript-serverless-api-from-serverless-framework-to-terraform-with-datadog-415i</guid>
      <description>&lt;p&gt;I originally built this project as a TypeScript CRUD API using the Serverless Framework, AWS Lambda, API Gateway, and DynamoDB.&lt;/p&gt;

&lt;p&gt;The application worked, but I wanted to understand the infrastructure at a deeper level instead of relying on one framework to generate most of the deployment configuration for me.&lt;/p&gt;

&lt;p&gt;I therefore rebuilt the infrastructure with Terraform and added Datadog observability so I could answer practical operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which Lambda function is slow?&lt;/li&gt;
&lt;li&gt;How much memory does each function use?&lt;/li&gt;
&lt;li&gt;What happens during a cold start?&lt;/li&gt;
&lt;li&gt;Can I follow one request from API Gateway through Lambda to DynamoDB?&lt;/li&gt;
&lt;li&gt;Can application logs be correlated with traces?&lt;/li&gt;
&lt;li&gt;How do I turn dashboard metrics into actionable alerts?&lt;/li&gt;
&lt;li&gt;How do I protect an existing DynamoDB table during &lt;code&gt;terraform destroy&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article walks through the migration step by step, including the code changes, Terraform structure, Datadog instrumentation, debugging, dashboards, monitors, and cleanup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Deployment status:&lt;/strong&gt; The demonstration infrastructure has been destroyed&lt;br&gt;
to avoid ongoing cloud costs. The Terraform configuration remains available&lt;br&gt;
for reproduction.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/Geocoder89/serverless-app" rel="noopener noreferrer"&gt;Serverless Products API on GitHub&lt;/a&gt;
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Starting from the Serverless Framework application&lt;/li&gt;
&lt;li&gt;Modernizing the Lambda handlers&lt;/li&gt;
&lt;li&gt;Organizing the Terraform project&lt;/li&gt;
&lt;li&gt;Planning and applying the infrastructure&lt;/li&gt;
&lt;li&gt;Securing the Datadog API key&lt;/li&gt;
&lt;li&gt;Debugging the Datadog Extension&lt;/li&gt;
&lt;li&gt;Building the Datadog dashboard&lt;/li&gt;
&lt;li&gt;Adding proactive monitors&lt;/li&gt;
&lt;li&gt;Cost controls and cleanup&lt;/li&gt;
&lt;li&gt;What I learned&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this migration, you will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 and npm&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;li&gt;AWS CLI credentials for a development account&lt;/li&gt;
&lt;li&gt;an existing DynamoDB table named &lt;code&gt;ProductsTable&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a Datadog account and API key&lt;/li&gt;
&lt;li&gt;Postman or another HTTP client&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;The application exposes five endpoints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Handler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createProduct&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/products&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;listProducts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/products/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getProductById&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/products/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateProduct&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/products/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deleteProduct&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client / Postman
       |
       v
API Gateway HTTP API
       |
       v
Five Node.js Lambda functions
       |
       v
Amazon DynamoDB

Datadog Lambda Library + Datadog Extension
       |
       +-- Distributed traces
       +-- Enhanced Lambda metrics
       +-- Structured logs
       +-- Dashboards
       +-- Threshold monitors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Final architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnv431n9n6djvgseba4xr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnv431n9n6djvgseba4xr.png" alt="Final Architecture" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Starting from the Serverless Framework application
&lt;/h2&gt;

&lt;p&gt;The original application used &lt;code&gt;serverless.yml&lt;/code&gt; to define Lambda functions, API Gateway routes, environment variables, IAM permissions, and packaging.&lt;/p&gt;

&lt;p&gt;A shortened example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#app metadata&lt;/span&gt;
&lt;span class="na"&gt;org&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;geocoderserverless&lt;/span&gt; 
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-serverless-app&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mini-ecommerce-api-serverless&lt;/span&gt;

&lt;span class="na"&gt;frameworkVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4"&lt;/span&gt;

&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs18.x&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ca-central-1&lt;/span&gt; &lt;span class="c1"&gt;## Change the ca-central to the region of choice&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;## This binds your env variable to the dynamoDB product table&lt;/span&gt;
    &lt;span class="na"&gt;PRODUCTS_TABLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ProductsTable&lt;/span&gt;  
&lt;span class="c1"&gt;## Lambda permissions.&lt;/span&gt;
  &lt;span class="na"&gt;iamRoleStatements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
      &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Scan&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:UpdateItem&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
      &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:dynamodb:ca-central-1:${aws:accountId}:table/ProductsTable&lt;/span&gt; &lt;span class="c1"&gt;## Change the ca-central to the region of choice&lt;/span&gt;
&lt;span class="c1"&gt;## handler build configuration&lt;/span&gt;
&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;individually&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;## This causes each lambda function to be built and packaged individually, this was done to save space&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/**&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.serverless/**&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.git/**&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test/**&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;README.md&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;

&lt;span class="c1"&gt;### Handler functions&lt;/span&gt;
&lt;span class="na"&gt;functions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;createProduct&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/handlers/createProduct.createProduct&lt;/span&gt; &lt;span class="c1"&gt;## The handler to create products&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post&lt;/span&gt;

  &lt;span class="na"&gt;listProducts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/handlers/listProducts.listProducts&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get&lt;/span&gt;

  &lt;span class="na"&gt;getProductById&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/handlers/getProductById.getProductById&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products/{id}&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get&lt;/span&gt;

  &lt;span class="na"&gt;updateProduct&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/handlers/updateProduct.updateProduct&lt;/span&gt;
    &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products/{id}&lt;/span&gt;
          &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;put&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Serverless Framework was useful because it provided a concise deployment abstraction. However, I wanted explicit control over the underlying AWS resources and lifecycle.&lt;/p&gt;

&lt;p&gt;The migration goals were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preserve the API behaviour.&lt;/li&gt;
&lt;li&gt;Preserve the existing DynamoDB table.&lt;/li&gt;
&lt;li&gt;Move infrastructure ownership to Terraform.&lt;/li&gt;
&lt;li&gt;Package each Lambda independently.&lt;/li&gt;
&lt;li&gt;Modernize the handlers to AWS SDK v3.&lt;/li&gt;
&lt;li&gt;Add structured logging.&lt;/li&gt;
&lt;li&gt;Add Datadog traces, metrics, dashboards, and monitors.&lt;/li&gt;
&lt;li&gt;Make cleanup safe and predictable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiygu774h0mehohun2pr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiygu774h0mehohun2pr2.png" alt="Original serverless configuration" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Modernizing the Lambda handlers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Move to AWS SDK v3
&lt;/h3&gt;

&lt;p&gt;Before migrating the infrastructure to Terraform, I first updated the application code.&lt;/p&gt;

&lt;p&gt;This gave me a working application baseline before introducing infrastructure and observability changes. It also made it easier to determine whether a problem came from the Lambda code, Terraform configuration, or Datadog instrumentation.&lt;/p&gt;

&lt;p&gt;The main changes were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;migrating to AWS SDK for JavaScript v3&lt;/li&gt;
&lt;li&gt;adding TypeScript types for API Gateway events and responses&lt;/li&gt;
&lt;li&gt;validating request payloads&lt;/li&gt;
&lt;li&gt;returning consistent HTTP responses&lt;/li&gt;
&lt;li&gt;adding structured JSON logs&lt;/li&gt;
&lt;li&gt;keeping the DynamoDB client outside the handler for execution-environment reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application contains five handlers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Handler&lt;/th&gt;
&lt;th&gt;DynamoDB operation&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createProduct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PutCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;listProducts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ScanCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns all products&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getProductById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GetCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieves one product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updateProduct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UpdateCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates an existing product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deleteProduct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DeleteCommand&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deletes a product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rather than reproducing the same initialization and response-handling code five times, I will walk through the complete &lt;code&gt;createProduct&lt;/code&gt; handler and then highlight the DynamoDB operation that makes each remaining handler different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing &lt;code&gt;createProduct&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The create handler performs the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads and validates the incoming JSON request.&lt;/li&gt;
&lt;li&gt;Generates a unique product ID.&lt;/li&gt;
&lt;li&gt;Builds the DynamoDB item.&lt;/li&gt;
&lt;li&gt;Stores the item using &lt;code&gt;PutCommand&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writes a structured application log.&lt;/li&gt;
&lt;li&gt;Returns a &lt;code&gt;201 Created&lt;/code&gt; response.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ConditionalCheckFailedException&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PutCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/lib-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyHandlerV2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CreateProductInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProductItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../interfaces/Product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/DynamoDbClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getProductsTableName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseJsonBody&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logWarn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateCreateProduct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/validation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Handler definition&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createProduct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyHandlerV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awsRequestId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parseJsonBody&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CreateProductInput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validationErrors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateCreateProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validationErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;logWarn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product validation failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createProduct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;validationErrors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validationErrors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProductItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`PRODUCT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attribute_not_exists(PK)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// send a success log&lt;/span&gt;
    &lt;span class="nf"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createProduct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_entityType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;publicProduct&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publicProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;SyntaxError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;logWarn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid JSON request body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createProduct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A product with this identifier already exists&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product creation failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createProduct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not create product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DynamoDB client is initialized outside the handler in &lt;a href="https://github.com/Geocoder89/serverless-app/blob/main/src/utils/DynamoDbClient.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/utils/DynamoDbClient.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/lib-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,{&lt;/span&gt;
  &lt;span class="na"&gt;marshallOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;removeUndefinedValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS Lambda may reuse an execution environment across multiple invocations. Keeping the client outside the handler allows later invocations to reuse it instead of creating a new client on every request.&lt;/p&gt;

&lt;p&gt;The product is persisted using &lt;code&gt;PutCommand&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attribute_not_exists(PK)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler delegates structured logging to the shared &lt;code&gt;logInfo&lt;/code&gt; utility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createProduct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0me13dwluaod3lmruh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0me13dwluaod3lmruh4.png" alt="Log configuration" width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The logger serializes the entry as JSON before writing it to CloudWatch. This gives every log a consistent timestamp, level, message, request ID, operation, and contextual fields.&lt;/p&gt;

&lt;p&gt;This structure becomes useful later when Datadog correlates the log with the trace that produced it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the remaining handlers
&lt;/h3&gt;

&lt;p&gt;The remaining handlers follow the same overall structure, but each uses a different DynamoDB command.&lt;/p&gt;

&lt;h4&gt;
  
  
  Listing products
&lt;/h4&gt;

&lt;p&gt;The list handler uses &lt;code&gt;ScanCommand&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScanCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
       &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
       &lt;span class="na"&gt;FilterExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#entityType = :entityType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#entityType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entityType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:entityType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;},&lt;/span&gt;
       &lt;span class="na"&gt;ExclusiveStartKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exclusiveStartKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scan is acceptable for this small learning project. For a production table with a large dataset, I would design the access pattern around &lt;code&gt;QueryCommand&lt;/code&gt; and an appropriate partition key or secondary index.&lt;/p&gt;

&lt;h4&gt;
  
  
  Retrieving a product
&lt;/h4&gt;

&lt;p&gt;The get handler uses the product ID to construct the DynamoDB key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`PRODUCT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;ConsistentRead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no item is returned, the handler responds with &lt;code&gt;404 Not Found&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Updating a product
&lt;/h4&gt;

&lt;p&gt;The update handler uses &lt;code&gt;UpdateCommand&lt;/code&gt; and returns the modified item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpdateCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`PRODUCT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attribute_exists(PK)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`SET &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;updateExpressions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ReturnValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ALL_NEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deleting a product
&lt;/h4&gt;

&lt;p&gt;The delete handler uses &lt;code&gt;DeleteCommand&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamoDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeleteCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProductsTableName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`PRODUCT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRODUCT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attribute_exists(PK)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete implementations of all five handlers are available in the &lt;a href="https://github.com/Geocoder89/serverless-app/tree/main/src/handlers" rel="noopener noreferrer"&gt;&lt;code&gt;src/handlers&lt;/code&gt;&lt;/a&gt; directory.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Building the Lambda bundles
&lt;/h2&gt;

&lt;p&gt;The project targets Node.js 22 and uses &lt;code&gt;esbuild&lt;/code&gt; to create one bundle per handler.&lt;/p&gt;

&lt;p&gt;The final TypeScript configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ESNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noEmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws-lambda"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowSyntheticDefaultImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resolveJsonModule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"forceConsistentCasingInFileNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"infra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"src-backup"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rm -rf dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typecheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc --noEmit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run clean &amp;amp;&amp;amp; esbuild src/handlers/*.ts --bundle --platform=node --target=node22 --format=cjs --outdir=dist --minify --tree-shaking=true --sourcemap=external --sources-content=false"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before writing Terraform, I verified the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me a clean baseline and five compiled JavaScript files.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9sxdmm2sk1s4xrg0y58s.png" alt="Serverless Typecheck and build" width="799" height="242"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  4. Organizing the Terraform project
&lt;/h2&gt;

&lt;p&gt;I separated the Terraform configuration by responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;infra/
├── api.tf
├── dynamodb.tf
├── iam.tf
├── lambda.tf
├── locals.tf
├── logs.tf
├── outputs.tf
├── providers.tf
├── variables.tf
└── versions.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform loads all &lt;code&gt;.tf&lt;/code&gt; files in the root module together, so this split is for maintainability rather than execution order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5x3shjsyjya61qgg6yu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5x3shjsyjya61qgg6yu0.png" alt="Project structure showing terraform project structure" width="800" height="1523"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Defining the functions once
&lt;/h2&gt;

&lt;p&gt;Instead of repeating the same Terraform structure five times, I defined the functions in a local map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createProduct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"createProduct.createProduct"&lt;/span&gt;
      &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POST /products"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;listProducts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"listProducts.listProducts"&lt;/span&gt;
      &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GET /products"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;getProductById&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"getProductById.getProductById"&lt;/span&gt;
      &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GET /products/{id}"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;updateProduct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"updateProduct.updateProduct"&lt;/span&gt;
      &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PUT /products/{id}"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;deleteProduct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deleteProduct.deleteProduct"&lt;/span&gt;
      &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DELETE /products/{id}"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This map drives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZIP archives&lt;/li&gt;
&lt;li&gt;Lambda functions&lt;/li&gt;
&lt;li&gt;API Gateway integrations&lt;/li&gt;
&lt;li&gt;API routes&lt;/li&gt;
&lt;li&gt;Lambda permissions&lt;/li&gt;
&lt;li&gt;CloudWatch log groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding an endpoint becomes a controlled change to one source of truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopehmkf5has26ku2t0n5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopehmkf5has26ku2t0n5.png" alt="Lambda functions defined in Terraform" width="800" height="976"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Protecting the existing DynamoDB table
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProductsTable&lt;/code&gt; already existed before the migration.&lt;/p&gt;

&lt;p&gt;I referenced it as a data source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_dynamodb_table"&lt;/span&gt; &lt;span class="s2"&gt;"products"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ProductsTable"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is critical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Terraform &lt;code&gt;resource&lt;/code&gt; is lifecycle-managed.&lt;/li&gt;
&lt;li&gt;A Terraform &lt;code&gt;data&lt;/code&gt; source is read-only from Terraform's perspective.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the table is a data source, &lt;code&gt;terraform destroy&lt;/code&gt; does not delete it.&lt;/p&gt;

&lt;p&gt;That was one of the most important safety decisions in the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Packaging and creating the Lambda functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Package one handler per ZIP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"archive_file"&lt;/span&gt; &lt;span class="s2"&gt;"lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zip"&lt;/span&gt;
  &lt;span class="nx"&gt;source_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/../dist/${each.key}.js"&lt;/span&gt;
  &lt;span class="nx"&gt;output_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/build/${each.key}.zip"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Independent packages keep deployment artifacts small and allow Terraform to detect function-specific code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create the functions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"product_handlers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-${each.key}-${var.environment}"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nodejs22.x"&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;

  &lt;span class="nx"&gt;filename&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;memory_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;PRODUCTS_TABLE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;common_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I first deployed this baseline without Datadog.&lt;/p&gt;

&lt;p&gt;That let me verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terraform was valid.&lt;/li&gt;
&lt;li&gt;All routes worked.&lt;/li&gt;
&lt;li&gt;DynamoDB reads and writes worked.&lt;/li&gt;
&lt;li&gt;CloudWatch logs worked.&lt;/li&gt;
&lt;li&gt;Any later issue could be isolated to the observability changes.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. IAM, API Gateway, and log groups
&lt;/h2&gt;

&lt;p&gt;The Lambda role received only the DynamoDB permissions used by the handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowProductsTableCrud"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:PutItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:Scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:UpdateItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"dynamodb:DeleteItem"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API Gateway integrations and routes also use the function map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_apigatewayv2_integration"&lt;/span&gt; &lt;span class="s2"&gt;"product_handlers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;api_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_apigatewayv2_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;integration_type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS_PROXY"&lt;/span&gt;
  &lt;span class="nx"&gt;integration_uri&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product_handlers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;invoke_arn&lt;/span&gt;
  &lt;span class="nx"&gt;integration_method&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;
  &lt;span class="nx"&gt;payload_format_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_apigatewayv2_route"&lt;/span&gt; &lt;span class="s2"&gt;"product_routes"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;api_id&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_apigatewayv2_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route_key&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"integrations/${aws_apigatewayv2_integration.product_handlers[each.key].id}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created the log groups explicitly so retention would not be indefinite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/aws/lambda/${aws_lambda_function.product_handlers[each.key].function_name}"&lt;/span&gt;
  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;common_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Planning and applying the infrastructure
&lt;/h2&gt;

&lt;p&gt;I initialized, formatted, validated, and planned the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra init
terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra &lt;span class="nb"&gt;fmt
&lt;/span&gt;terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra validate
&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first deployment plan showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;30 to add, 0 to change, 0 to destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important part was &lt;code&gt;0 to destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The existing DynamoDB table appeared as a data source, not as a resource to replace or delete.&lt;br&gt;
The plan below shows the proposed infrastructure changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogaqh020z3fckgbsl980.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogaqh020z3fckgbsl980.png" alt="Terraform plan" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra apply tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform created the Lambda functions, API Gateway routes, IAM resources, permissions, and log groups.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2wy3ti67fhnir5p8tw5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2wy3ti67fhnir5p8tw5i.png" alt="Terraform apply" width="799" height="246"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Verifying the Deployed CRUD API with Postman
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST   /products
GET    /products
GET    /products/{id}
PUT    /products/{id}
DELETE /products/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a product.&lt;/li&gt;
&lt;li&gt;Capture the returned ID.&lt;/li&gt;
&lt;li&gt;List products.&lt;/li&gt;
&lt;li&gt;Retrieve the product.&lt;/li&gt;
&lt;li&gt;Update it.&lt;/li&gt;
&lt;li&gt;Retrieve the updated version.&lt;/li&gt;
&lt;li&gt;Delete it.&lt;/li&gt;
&lt;li&gt;Confirm it no longer exists.&lt;/li&gt;
&lt;li&gt;Send invalid input and verify a controlled &lt;code&gt;400&lt;/code&gt; response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the CRUD Handlers with Postman
&lt;/h2&gt;

&lt;p&gt;Before adding Datadog instrumentation, I tested the Terraform-deployed API through API Gateway using Postman.&lt;/p&gt;

&lt;p&gt;The goal was to verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway routed each request to the correct Lambda function&lt;/li&gt;
&lt;li&gt;the handlers accepted and validated the request data&lt;/li&gt;
&lt;li&gt;DynamoDB operations completed successfully&lt;/li&gt;
&lt;li&gt;the API returned the expected HTTP status codes&lt;/li&gt;
&lt;li&gt;the product lifecycle worked from creation through deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating a product
&lt;/h3&gt;

&lt;p&gt;I first sent a &lt;code&gt;POST&lt;/code&gt; request to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request body contained test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mechanical Keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless mechanical keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;89.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned &lt;code&gt;201 Created&lt;/code&gt; together with the generated product ID and timestamps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u74mnuu4ha54q8ihd6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u74mnuu4ha54q8ihd6k.png" alt="Postman showing a successful POST request that created a product" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;code&gt;createProduct&lt;/code&gt; Lambda returned &lt;code&gt;201 Created&lt;/code&gt; after persisting the new product in DynamoDB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Below is the DynamoDB table showing the Product item created:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyaa4ffz0on6n4sfoma3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyaa4ffz0on6n4sfoma3m.png" alt="DynamoDB table" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I saved the returned ID as a Postman environment variable so it could be reused by the remaining requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;productId = &amp;lt;ID returned by POST /products&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Listing Products
&lt;/h3&gt;

&lt;p&gt;The GET /products endpoint returned the current list of products.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwy41fkcmmnfz1jnxjl7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwy41fkcmmnfz1jnxjl7.png" alt="List products terraform" width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving the created product
&lt;/h3&gt;

&lt;p&gt;I then sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /products/{{productId}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned &lt;code&gt;200 OK&lt;/code&gt; and the product created in the previous request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38ubf0c68rj7e26y70bp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38ubf0c68rj7e26y70bp.png" alt="Postman showing the product retrieved by ID" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;code&gt;getProductById&lt;/code&gt; Lambda retrieved the product using its DynamoDB partition and sort keys.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the product
&lt;/h3&gt;

&lt;p&gt;To test the update handler, I sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT /products/{{productId}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the following body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updated wireless mechanical keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;99.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned the modified product with a new &lt;code&gt;updatedAt&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89e1xmkb9hph1evrkm6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89e1xmkb9hph1evrkm6g.png" alt="Postman showing a successful product update" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;code&gt;updateProduct&lt;/code&gt; Lambda updated the existing DynamoDB item and returned the new values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgcior5di3bjmzt432ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgcior5di3bjmzt432ii.png" alt="Postman showing the updated product retrieved successfully" width="799" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deleting the product
&lt;/h3&gt;

&lt;p&gt;Finally, I sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DELETE /products/{{productId}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API confirmed that the product had been deleted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqh6dv1h8jrumxjcsn6c4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqh6dv1h8jrumxjcsn6c4.png" alt="Postman showing a successful DELETE request" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;code&gt;deleteProduct&lt;/code&gt; Lambda removed the product from DynamoDB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To verify that the deletion was complete, I repeated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /products/{{productId}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product not found"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a &lt;code&gt;404 Not Found&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6p49tr5oo1ci5d1y3wd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6p49tr5oo1ci5d1y3wd.png" alt="Postman showing a 404 response after the product was deleted" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The final &lt;code&gt;404&lt;/code&gt; response confirms that the product was no longer present in DynamoDB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I also submitted a product-creation request that violated the validation rules. The API returned 400 Bad Request with details explaining why the request was rejected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6u5rhli308m7biik69x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6u5rhli308m7biik69x.png" alt="Invalid post product terraform" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Securing the Datadog API key
&lt;/h2&gt;

&lt;p&gt;The Datadog API key must not be stored in Git, Terraform variables, screenshots, or plaintext Lambda environment variables.&lt;/p&gt;

&lt;p&gt;I stored it as an AWS Systems Manager Parameter Store &lt;code&gt;SecureString&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/serverless-products-api/dev/datadog-api-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safe shell pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"DD_API_KEY?Paste the Datadog API key: "&lt;/span&gt;
&lt;span class="nb"&gt;echo

&lt;/span&gt;&lt;span class="nv"&gt;CLEAN_DD_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DD_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal aws ssm put-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/serverless-products-api/dev/datadog-api-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; &lt;span class="s2"&gt;"SecureString"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLEAN_DD_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ca-central-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--overwrite&lt;/span&gt;

&lt;span class="nb"&gt;unset &lt;/span&gt;DD_API_KEY CLEAN_DD_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform constructs the ARN without reading the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;datadog_api_key_parameter_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/serverless-products-api/dev/datadog-api-key"&lt;/span&gt;

  &lt;span class="nx"&gt;datadog_api_key_ssm_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"arn:aws:ssm:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;":parameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_api_key_parameter_name&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda execution role receives ssm:GetParameter for the specific parameter and kms:Decrypt for the KMS key used to encrypt it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"ssm:GetParameter"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_api_key_ssm_arn&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_kms_alias&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target_key_arn&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Adding the Datadog Lambda layers
&lt;/h2&gt;

&lt;p&gt;The Datadog library and Extension layer versions depend on the runtime, region, architecture, and current Datadog release.&lt;br&gt;
Because Datadog publishes new versions over time, verify the current Node.js and Extension layer versions before deploying.&lt;/p&gt;

&lt;p&gt;The following excerpt shows the Datadog-specific additions to the existing Lambda resource.&lt;br&gt;
I represented them as variables,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"datadog_node_layer_version"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Datadog Node.js Lambda library layer version."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"datadog_extension_layer_version"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Datadog Lambda Extension layer version."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;98&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda resource then uses the Datadog wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"product_handlers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;

  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-${each.key}-${var.environment}"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nodejs22.x"&lt;/span&gt;

  &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/opt/nodejs/node_modules/datadog-lambda-js/handler.handler"&lt;/span&gt;

  &lt;span class="nx"&gt;layers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_node_layer_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_extension_layer_arn&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;PRODUCTS_TABLE&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="nx"&gt;DD_LAMBDA_HANDLER&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;
      &lt;span class="nx"&gt;DD_SERVICE&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;
      &lt;span class="nx"&gt;DD_ENV&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
      &lt;span class="nx"&gt;DD_VERSION&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_version&lt;/span&gt;
      &lt;span class="nx"&gt;DD_SITE&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_site&lt;/span&gt;
      &lt;span class="nx"&gt;DD_API_KEY_SSM_ARN&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datadog_api_key_ssm_arn&lt;/span&gt;
      &lt;span class="nx"&gt;DD_TRACE_ENABLED&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
      &lt;span class="nx"&gt;DD_LOGS_ENABLED&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
      &lt;span class="nx"&gt;DD_ENHANCED_METRICS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;

      &lt;span class="nx"&gt;DD_CAPTURE_LAMBDA_PAYLOAD&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DD_LAMBDA_HANDLER&lt;/code&gt; tells the Datadog wrapper which original application handler to invoke.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhjn7w27ky6up6zafz7f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhjn7w27ky6up6zafz7f.png" alt="Datadog terraform plan" width="799" height="246"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Debugging the Datadog Extension crash
&lt;/h2&gt;

&lt;p&gt;After the first Datadog deployment, CloudWatch showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELEMETRY Name: datadog-agent State: Subscribed
EXTENSION Name: datadog-agent State: Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application also generated trace and span IDs.&lt;/p&gt;

&lt;p&gt;However, the invocation ended with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extension.Crash
failed to parse header: InvalidHeaderValue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This indicated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the library was instrumenting the Lambda&lt;/li&gt;
&lt;li&gt;the Extension started&lt;/li&gt;
&lt;li&gt;the application operation completed&lt;/li&gt;
&lt;li&gt;telemetry flushing failed afterward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The root cause was a malformed API-key value in Parameter Store.&lt;/p&gt;

&lt;p&gt;A hidden newline, surrounding whitespace, or incorrectly copied value can become an invalid HTTP header.&lt;/p&gt;

&lt;p&gt;After sanitizing and overwriting the parameter, CloudWatch showed a healthy invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TELEMETRY Name: datadog-agent State: Subscribed
EXTENSION Name: datadog-agent State: Ready
START RequestId: ...
INFO [dd.trace_id=... dd.span_id=...] Product created
END RequestId: ...
REPORT RequestId: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson was simple:&lt;/p&gt;

&lt;p&gt;A secret can exist and be retrievable while still being invalid for the application consuming it.&lt;/p&gt;

&lt;p&gt;The final CloudWatch logs below confirm that the Extension initialized successfully and the Lambda invocation completed normally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqyo2uygg8zk3p6jp2y9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqyo2uygg8zk3p6jp2y9.png" alt="Healthy cloud watch logs Datadog" width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Verifying end-to-end traces
&lt;/h2&gt;

&lt;p&gt;I generated traffic for all five routes.&lt;/p&gt;

&lt;p&gt;Datadog received traces for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /products
GET /products
GET /products/{id}
PUT /products/{id}
DELETE /products/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;POST /products&lt;/code&gt; trace showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
  → Lambda
  → application handler
  → DynamoDB PutItem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more useful than an isolated metric because it explains where time was spent during one request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyo65307dj5ina7l8pwzp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyo65307dj5ina7l8pwzp.png" alt="redacted-trace" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Verifying trace-to-log correlation
&lt;/h2&gt;

&lt;p&gt;Inside the Lambda span, Datadog displayed the structured application log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dd.trace_id=...
dd.span_id=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The troubleshooting path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slow or failed request
  → affected span
  → associated log
  → request ID and product ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below is an example showing traces, spans and a correlated structured log and its associated metadata:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx94s8jkrlf5bytk2n966.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx94s8jkrlf5bytk2n966.png" alt="Datadog traces/spans" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  16. Building the Datadog dashboard
&lt;/h2&gt;

&lt;p&gt;I created a dashboard named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Serverless Products API — Dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It tracks:&lt;/p&gt;

&lt;h3&gt;
  
  
  Traffic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda invocations&lt;/li&gt;
&lt;li&gt;API requests&lt;/li&gt;
&lt;li&gt;API errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;average Lambda runtime&lt;/li&gt;
&lt;li&gt;runtime by Lambda function&lt;/li&gt;
&lt;li&gt;API latency percentiles&lt;/li&gt;
&lt;li&gt;cold-start initialization duration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;peak memory by Lambda function&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Serverless overhead
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Datadog Extension post-runtime duration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;estimated Lambda cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main enhanced metrics were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws.lambda.enhanced.invocations
aws.lambda.enhanced.runtime_duration
aws.lambda.enhanced.init_duration
aws.lambda.enhanced.post_runtime_duration
aws.lambda.enhanced.max_memory_used
aws.lambda.enhanced.estimated_cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Making small cost values readable
&lt;/h3&gt;

&lt;p&gt;For a low-volume development workload, the estimated cost was far below one cent.&lt;/p&gt;

&lt;p&gt;Four decimal places displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$0.0000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used a formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a * 1000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and labelled the result as micro-US dollars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;μUSD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;94.13 μUSD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes only the display scale, not the underlying cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faork0i07570cewmqwr3p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faork0i07570cewmqwr3p.png" alt="Datadog-Lambda-cost-calculation-formula" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final dashboard below displays the key metrics described above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ikc0hhfhnjs7n7hqn0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4ikc0hhfhnjs7n7hqn0.png" alt="Datadog-dashboard-key-metrics" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Adding proactive monitors
&lt;/h2&gt;

&lt;p&gt;A dashboard requires someone to look at it.&lt;/p&gt;

&lt;p&gt;A monitor evaluates telemetry and changes state when a threshold is crossed.&lt;/p&gt;

&lt;p&gt;I created three monitors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor 1: High p95 latency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monitor:    [Dev] Serverless Products API — High Lambda Latency
Scope:      env:dev, service:serverless-products-api
Metric:     p95 trace.aws.lambda latency
Evaluation: MAX over the last 1 hour
Warning:    above 750 ms
Critical:   above 1 second
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The observed latency was around 500 ms, so 750 ms represented degradation and 1 second represented a critical threshold.&lt;/p&gt;

&lt;p&gt;For sparse development traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show last known status
Do not require a full window
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvotd0ycni1vbj991f51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvotd0ycni1vbj991f51.png" alt="dashboard-high-latency" width="799" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor 2: High error rate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monitor:    [Dev] Serverless Products API — High Error Rate
Scope:      env:dev, service:serverless-products-api
Metric:     Lambda APM error rate
Evaluation: SUM over the last 1 hour
Warning:    above 5%
Critical:   above 10%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this APM error-rate monitor, Datadog used SUM because the underlying request and error metrics are count-based.&lt;/p&gt;

&lt;p&gt;A low-traffic limitation is important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 failed request / 10 total requests = 10%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production monitor should also require a minimum request count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0sf63oaze3w83mbttf8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0sf63oaze3w83mbttf8.png" alt="Datadog-error-rate" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor 3: High memory by function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monitor:    [Dev] Serverless Products API — High Lambda Memory Usage
Metric:     aws.lambda.enhanced.max_memory_used
Filter:     functionname:serverless-products-api-*
Group by:   functionname
Evaluation: MAX over the last 1 hour
Warning:    above 180 MiB
Critical:   above 205 MiB
Allocation: 256 MiB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grouping by &lt;code&gt;functionname&lt;/code&gt; creates a separate monitor group for every Lambda function.&lt;/p&gt;

&lt;p&gt;The notification template includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Affected function: {{functionname.name}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Debugging a no-data monitor
&lt;/h3&gt;

&lt;p&gt;The memory monitor initially showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NO DATA
No groups reporting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard already had memory data, so the telemetry pipeline was working.&lt;/p&gt;

&lt;p&gt;The problem was a typo in the filter.&lt;/p&gt;

&lt;p&gt;Incorrect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;functionname:serverless-api-*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;functionname:serverless-products-api-*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the correction, the monitor displayed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a useful reminder that a monitoring failure can come from a query mismatch rather than an application failure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfopky3um6tiidy40yxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgfopky3um6tiidy40yxw.png" alt="Datadog lambda usage-metrics" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  18. Cost controls and cleanup
&lt;/h2&gt;

&lt;p&gt;Before deploying, I configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Budget alerts&lt;/li&gt;
&lt;li&gt;AWS Free Tier alerts&lt;/li&gt;
&lt;li&gt;cost anomaly monitoring&lt;/li&gt;
&lt;li&gt;seven-day CloudWatch retention&lt;/li&gt;
&lt;li&gt;low-volume test traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A budget alert is not a hard spending cap.&lt;/p&gt;

&lt;p&gt;The strongest control remains removing resources when testing is finished.&lt;/p&gt;

&lt;p&gt;Preview destruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-destroy&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;destroy.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Destroy Terraform-managed resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;infra apply destroy.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing DynamoDB table remains because it is a data source.&lt;/p&gt;

&lt;p&gt;The manually created SSM parameter must be deleted separately when it is no longer needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;personal aws ssm delete-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/serverless-products-api/dev/datadog-api-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ca-central-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Datadog dashboard and monitors remain because they were created manually.&lt;/p&gt;

&lt;p&gt;The screenshots below show the reviewed destruction plan and its successful application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1rqlvb0bd8gz73i9onm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1rqlvb0bd8gz73i9onm.png" alt="terraform-destroy-plan" width="799" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mg7koek1xy1ufhueece.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mg7koek1xy1ufhueece.png" alt="terraform-destroy-apply" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Framework abstraction and infrastructure ownership are different skills
&lt;/h3&gt;

&lt;p&gt;The Serverless Framework helped me deploy the original application quickly.&lt;/p&gt;

&lt;p&gt;Terraform forced me to understand the individual AWS resources, dependencies, permissions, and lifecycle decisions underneath that abstraction.&lt;/p&gt;

&lt;p&gt;The point was not to prove that one tool is universally better. The value was understanding what each tool hides and when explicit infrastructure ownership is useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Terraform plan is a safety mechanism
&lt;/h3&gt;

&lt;p&gt;The most important line in the first plan was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 to destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with the DynamoDB data source, that gave me confidence that existing data would survive the migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability should be verified in layers
&lt;/h3&gt;

&lt;p&gt;I verified the Datadog integration in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CloudWatch showed the Extension starting.&lt;/li&gt;
&lt;li&gt;structured logs contained trace and span IDs.&lt;/li&gt;
&lt;li&gt;Datadog received Lambda spans.&lt;/li&gt;
&lt;li&gt;traces included DynamoDB.&lt;/li&gt;
&lt;li&gt;logs appeared inside trace details.&lt;/li&gt;
&lt;li&gt;enhanced metrics populated the dashboard.&lt;/li&gt;
&lt;li&gt;monitors evaluated live telemetry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This made debugging much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  No data does not always mean no telemetry
&lt;/h3&gt;

&lt;p&gt;The memory metric existed, but the monitor returned no groups because its filter did not match the real function names.&lt;/p&gt;

&lt;p&gt;When a monitor has no data, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tag names&lt;/li&gt;
&lt;li&gt;tag values&lt;/li&gt;
&lt;li&gt;wildcard placement&lt;/li&gt;
&lt;li&gt;grouping&lt;/li&gt;
&lt;li&gt;evaluation window&lt;/li&gt;
&lt;li&gt;full-window requirements&lt;/li&gt;
&lt;li&gt;sparse metric behaviour&lt;/li&gt;
&lt;li&gt;whether edits were saved&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Thresholds require context
&lt;/h3&gt;

&lt;p&gt;A threshold should relate to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;observed baseline behaviour&lt;/li&gt;
&lt;li&gt;allocated resources&lt;/li&gt;
&lt;li&gt;traffic volume&lt;/li&gt;
&lt;li&gt;expected workload&lt;/li&gt;
&lt;li&gt;business impact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-hour windows were appropriate for this sparse development workload.&lt;/p&gt;

&lt;p&gt;A production environment would likely use shorter sustained windows, minimum request thresholds, and environment-specific policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Possible next improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Manage Datadog dashboards and monitors through the Datadog Terraform provider.&lt;/li&gt;
&lt;li&gt;Add CI/CD for type-checking, builds, Terraform validation, and deployment.&lt;/li&gt;
&lt;li&gt;Upload source maps for clearer TypeScript stack traces.&lt;/li&gt;
&lt;li&gt;Add automated integration tests.&lt;/li&gt;
&lt;li&gt;Add pagination.&lt;/li&gt;
&lt;li&gt;Replace scans with query-based DynamoDB access patterns where appropriate.&lt;/li&gt;
&lt;li&gt;Add authentication and authorization.&lt;/li&gt;
&lt;li&gt;Add remote Terraform state and locking.&lt;/li&gt;
&lt;li&gt;Add a minimum traffic condition to the error-rate monitor.&lt;/li&gt;
&lt;li&gt;Add deployment markers to correlate releases with performance changes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final result
&lt;/h2&gt;

&lt;p&gt;The project now demonstrates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript development
  → AWS SDK v3 modernization
  → esbuild packaging
  → Terraform infrastructure
  → API Gateway and Lambda deployment
  → DynamoDB persistence
  → secure secret retrieval
  → Datadog tracing and metrics
  → structured log correlation
  → dashboarding
  → threshold monitoring
  → cost-aware cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of stopping when the API returned a successful response, I could now trace the request, inspect the DynamoDB dependency, find the corresponding application log, measure runtime and memory, estimate cost, and define an alert when performance or resource usage crossed a threshold.&lt;/p&gt;

&lt;p&gt;That is the difference between deploying an application and operating one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Article version:&lt;/strong&gt; &lt;a href="https://github.com/Geocoder89/serverless-app/tree/v1.0.0-terraform-datadog" rel="noopener noreferrer"&gt;v1.0.0-terraform-datadog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




</description>
      <category>terraform</category>
      <category>datadog</category>
      <category>aws</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Building Serverless APIS with Serverless,Node.js, Typescript, DynamoDB, and Lambda.</title>
      <dc:creator>Oladele Omoarukhe</dc:creator>
      <pubDate>Mon, 06 Jan 2025 01:15:22 +0000</pubDate>
      <link>https://dev.to/oladele_omoarukhe_9c700bf/building-serverless-apis-with-serverlessnodejs-typescript-dynamodb-and-lambda-54ni</link>
      <guid>https://dev.to/oladele_omoarukhe_9c700bf/building-serverless-apis-with-serverlessnodejs-typescript-dynamodb-and-lambda-54ni</guid>
      <description>&lt;p&gt;Node.js has become the go-to platform for building server-side applications with JavaScript. Its ecosystem, starting with foundational tools like Express, has evolved significantly, offering diverse frameworks and libraries catering to various design paradigms. Traditionally, building APIs involved writing code to configure and manage servers.&lt;/p&gt;

&lt;p&gt;However, the rise of serverless architectures has shifted this paradigm, enabling developers to focus on application logic without worrying about server management. This approach has gained tremendous popularity, especially with platforms like AWS.&lt;/p&gt;

&lt;p&gt;Despite its advantages, beginners often face challenges finding clear resources to start building serverless APIs. This article aims to bridge that gap by providing a step-by-step guide to creating serverless API endpoints for a mini e-commerce application using AWS.&lt;br&gt;
It specifically provides a step-by-step guide to building serverless CRUD API endpoints for managing products in a mini e-commerce application using AWS.&lt;/p&gt;

&lt;p&gt;Below are a list of technologies required to build this and how they contribute to the entire makeup.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; - Provides the runtime environment for our application code. It is lightweight and fast for server-side JavaScript.It provides excellent support for asynchronous programming, which is crucial for handling HTTP requests and DynamoDB queries efficiently. It also helps to handle requests and responses using AWS Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt;- A superset of Javascript, it adds static typing and modern JavaScript features to the codebase thus improving code quality and maintainability.
It also detects type-related errors during development, reducing runtime bugs. It also provides better autocompletion and error detection (amazing stuff!😃).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt; - It helps execute our application code in a serverless manner. It removes the need to manage servers as well as the additional advantage that It scales automatically with usage. It is also cost-effective since you essentially only pay for execution time. Each API operation (e.g., creating or listing products) runs as a separate Lambda function.It also has a smooth integration with &lt;em&gt;AWS API Gateway&lt;/em&gt; to respond to HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS API Gateway&lt;/strong&gt; - Acts as the entry point for HTTP requests to our Lambda functions. It exposes Lambda functions as RESTful APIs.
Provides features like request validation, rate limiting, and authorization.It routes HTTP requests (e.g., POST /products) to the appropriate Lambda function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS DynamoDB&lt;/strong&gt; - Stores data for the application (e.g., product information). NoSQL database designed for high performance and scalability. Also, it's "Pay-per-use" model aligns with the serverless approach. Each product has a Partition Key (PK) for efficient lookups and a Sort Key (SK) for additional structuring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless Framework&lt;/strong&gt; - It simplifies the deployment and management of serverless applications. It majorly abstracts away AWS-specific configurations. It also provides a unified YAML-based configuration file to define resources, functions, and plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS SDK&lt;/strong&gt; - It allows the application to interact with AWS services programmatically. This provides APIs for DynamoDB,Lambda, S3, and other AWS services. It simplifies authentication and communication with AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postman&lt;/strong&gt; - Tool to test the API endpoints.It verifies the functionality of the API during development and after deployment. This is achieved by sending POST, GET, or other HTTP requests to endpoints and examining responses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article expects that you have an AWS account set up with most preferably an IAM user set up which is not the root-user (to follow the least privilege approach to AWS user/resource management). It also requires you have your account configured using the AWS-CLI. This cannot be covered here but there are amazing resources on the internet to learn this. Hopefully I can cover this in a future article as well.&lt;/p&gt;

&lt;p&gt;Without further ado, Let's dive in 🚀&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;PREREQUISITES&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we begin coding, It is highly important that we have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have the latest installed version of the Node.js runtime, you can find it &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;We have an AWS account set up as mentioned above, you can start &lt;a href="https://aws.amazon.com/resources/create-account/" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;We also need to have &lt;em&gt;serverless&lt;/em&gt; installed globally using the following command (In some cases you would need to add sudo or run administrator access (For Windows) as you might need root permissions
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g serverless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;You then need to set up the &lt;strong&gt;AWS CLI&lt;/strong&gt; by first installing it &lt;a href="https://aws.amazon.com/cli/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and configure it with your AWS access details using the command below:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;We are then required to enter our AWS Access Key, Secret Key, region, and default output format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2)  &lt;strong&gt;PROJECT SETUP&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With these basic pre-requisites in place, we are then required to create our new serverless project template using the command below.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serverless 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I personally advise selecting the Node.js, Express and DynamoDB template, to reduce the boilerplate code needed to bootstrap the application , an example is shown below&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgp13c5a5lwped8o0jxxq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgp13c5a5lwped8o0jxxq.png" alt="Serverless-setup" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I named the created project &lt;em&gt;mini-ecommerce-api&lt;/em&gt; (you can select any name of choice). It creates a folder of the same name which one can navigate to as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd mini-ecommerce-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then proceed to install required dependencies and dev-dependencies in addition to those installed when creating the project as follows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install ts-node @types/node aws-sdk
npm install --save-dev typescript @types/aws-lambda

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a breakdown of the major packages installed&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Typescript&lt;/em&gt;: TypeScript support.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;ts-node&lt;/em&gt;: Runs TypeScript directly without compiling to JS first.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;@types/node&lt;/em&gt;: Type definitions for Node.js.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;aws-sdk&lt;/em&gt;: AWS library to interact with AWS services like DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;@types/aws-lambda&lt;/em&gt;: Provides type support for writing AWS Lambda functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3) &lt;strong&gt;Configure Typescript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx tsc --init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;em&gt;tsconfig.json&lt;/em&gt; which we then edit as follows&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) &lt;strong&gt;Project Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We then proceed to create files and folders in the structure below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mini-ecommerce-api/
├── src/
│   ├── handlers/
│   │   ├── createProduct.ts
│   │   ├── listProducts.ts
        ├── getProductId.ts
        ├── updateProduct.ts
        ├── deleteProduct.ts
│   ├── utils/
│   │   ├── dynamoClient.ts
├── .gitignore
├── serverless.yml
├── tsconfig.json
├── package.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5) &lt;strong&gt;Configure Serverless.yml&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;We then replace default serverless.yml file generated in the project to the following which gives a proper definition to what we are building for this example. This example can be adjusted to your specific project need(s)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#app metadata
org: geocoderserverless 
app: my-serverless-app
service: mini-ecommerce-api-serverless

frameworkVersion: "4"

provider:
  name: aws
  runtime: nodejs18.x
  region: ca-central-1 ## Change ca-central to your  region of choice
  environment: ## This binds your env variable to the dynamoDB product table
    PRODUCTS_TABLE:
      Ref: ProductsTable  
## Lambda permissions.
  iamRoleStatements: 
    - Effect: Allow
      Action:
        - dynamodb:PutItem
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: arn:aws:dynamodb:ca-central-1:${aws:accountId}:table/ProductsTable ## Change the ca-central to the region of choice
## handler build configuration

## This causes each lambda function to be built and packaged individually, this was done to save space
package: 
  individually: true 
  exclude:
    - node_modules/**
    - .serverless/**
    - .git/**
    - test/**
    - README.md
    - package-lock.json

## Handler functions
functions:
  createProduct:
    handler: src/handlers/createProduct.createProduct ## The handler to create products
    events:
      - http:
          path: products
          method: post

  listProducts:
    handler: src/handlers/listProducts.listProducts
    events:
      - http:
          path: products
          method: get

  getProductById:
    handler: src/handlers/getProductById.getProductById
    events:
      - http:
          path: products/{id}
          method: get

  updateProduct:
    handler: src/handlers/updateProduct.updateProduct
    events:
      - http:
          path: products/{id}
          method: put

  deleteProduct:
    handler: src/handlers/deleteProduct.deleteProduct
    events:
      - http:
          path: products/{id}
          method: delete

#DynamoDB table configuration
resources:
  Resources:
    ProductsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ProductsTable
        AttributeDefinitions:
          - AttributeName: PK
            AttributeType: S
          - AttributeName: SK
            AttributeType: S
        KeySchema:
          - AttributeName: PK
            KeyType: HASH
          - AttributeName: SK
            KeyType: RANGE
        BillingMode: PAY_PER_REQUEST

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;6)  &lt;strong&gt;Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With the major part of the configuration sorted, we can then proceed to write the code. &lt;/p&gt;

&lt;p&gt;a) &lt;em&gt;DynamoDBClient set up (src/utils/DynamoDBClient.ts)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;First of all we need code to make database calls to DynamoDB, so we write the following to create a dynamoDBClient utility function which helps us abstract the logic to interact with DynamoDB as follows:&lt;/p&gt;

&lt;p&gt;src/utils/DynamoDBClient.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { DynamoDB } from "aws-sdk";

const dynamoDB = new DynamoDB.DocumentClient()

export default dynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) &lt;em&gt;Create a Product Interface (src/utils/DynamoDBClient.ts)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For proper type definitions, We can choose to create an interface which defines what a product would require, this helps provide type safety when writing our Lambda functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export interface Product {
  name: string;
  price: number;
  description?: string  
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;c) With this set up, we can proceed to create handlers which call lambda functions to execute our CRUD operations. The handler definitions are shown below.&lt;/p&gt;

&lt;p&gt;i. &lt;em&gt;Create Product Handler (src/handlers/createProduct.ts)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { APIGatewayProxyHandler } from 'aws-lambda';
import dynamoDB from '../utils/DynamoDbClient';
import { Product } from '../interfaces/Product';
import { DynamoDB } from 'aws-sdk';

// Handler definition
export const createProduct: APIGatewayProxyHandler = async (event) =&amp;gt; {
  const body: Partial&amp;lt;Product&amp;gt; = JSON.parse(event.body || '{}');
  const { name, price, description } = body;
  if (!name || !price) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'Name and price are required!' }),
    };
  }

// Definition of parameters including input 
  const params: DynamoDB.DocumentClient.PutItemInput = {
    TableName: process.env.PRODUCTS_TABLE!,
    Item: {
      PK: `PRODUCT#${name}`,
      SK: `PRODUCT`,
      name,
      price,
      description,
    } as Product,
  };



  try {
    await dynamoDB.put(params).promise();
    return {
      statusCode: 201,
      body: JSON.stringify({
        message: 'Product created successfully',
        data: params.Item,
      }),
    };
  } catch (error) {
    console.error('Error creating product:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        error: 'Could not create product',
      }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ii. &lt;em&gt;List Products Handler (src/handlers/listProducts.ts)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { APIGatewayProxyHandler } from 'aws-lambda'
import dynamoDB from '../utils/DynamoDbClient'
import {DynamoDB} from 'aws-sdk'
import { Product } from '../interfaces/Product'
export const listProducts: APIGatewayProxyHandler = async(event)=&amp;gt; {

 const params: DynamoDB.DocumentClient.ScanInput = {

      TableName: process.env.PRODUCTS_TABLE!,
    }

    try {
     const result: DynamoDB.DocumentClient.ScanOutput = await dynamoDB.scan(params).promise()
     const products: Product[] = result.Items as Product[] || []
     return {
      statusCode: 200,
      body: JSON.stringify({
        success: "true",
        data: products
      })
     }
    } catch (error) {
      console.error('Error Fetching products:',error)
      return {
        statusCode: 500,
        body: JSON.stringify({
          error: 'Could not fetch products'
        })
      }
    }
  } 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iii. &lt;em&gt;Fetch Products by ID (PK) Handler (src/handlers/getProductById.ts)&lt;/em&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For ease of understanding this article I chose to make the name of the product the Primary/Partition Key (PK) and the implementation for this is shown below.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { APIGatewayProxyHandler } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";
import { Product } from "../interfaces/Product";

export const getProductById: APIGatewayProxyHandler = async(event)=&amp;gt;{

const productId = event.pathParameters?.id

  if(!productId) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        error: "Product ID is required"
      })
    }
  }

  const params: DynamoDB.DocumentClient.GetItemInput = {
    TableName: process.env.PRODUCTS_TABLE!,
    Key: {
      PK: `PRODUCT#${productId}`,
      SK: "PRODUCT"
    }
  }

  try {

    const result: DynamoDB.DocumentClient.GetItemOutput = await dynamoDB.get(params).promise()
    const product: Product | undefined = result.Item as Product

    if(!product) {
      return {
        statusCode: 404,
        body: JSON.stringify({
          error: "Product not found!"
        })
      }
    }

    return {
      statusCode: 200,
      body: JSON.stringify({
        success: true,
        data: product
      })
    }

  } catch (error) {
    console.error('Error fetching product:', error)
    return {
      statusCode: 500,
      body: JSON.stringify({error: `Could not fetch product with id ${productId}`})
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iv &lt;em&gt;Update a product by ID handler (src/handlers/updateProduct.ts)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { APIGatewayProxyHandler } from "aws-lambda";
import { Product } from "../interfaces/Product";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";

export const updateProduct: APIGatewayProxyHandler = async (event)=&amp;gt;{
  const productId = event.pathParameters?.id
  const body: Partial&amp;lt;Product&amp;gt; = JSON.parse(event.body || "{}")

  const {name, price, description} = body

  if(!productId || (!name &amp;amp;&amp;amp; !price &amp;amp;&amp;amp; !description)) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        error: 'PRODUCT ID and least one field should be updated'
      })
    }
  }

  // set up to store edited values
  const updateExpressions: string[] = []
  const expressionAttributeNames: Record&amp;lt;string,string&amp;gt; = {}
  const expressionAttributeValues: Record&amp;lt;string,any&amp;gt; = {}

  if(name) {
    updateExpressions.push("#name = :name")
    expressionAttributeNames["#name"] = "name"
    expressionAttributeValues[":name"] = name

  }
  if(description) {
    updateExpressions.push("#description = :description")
    expressionAttributeNames["#description"] = "description"
    expressionAttributeValues[":description"] = description

  }
  if(price) {
    updateExpressions.push("#price = :price")
    expressionAttributeNames["#price"] = "price"
    expressionAttributeValues[":price"] = price

  }

  // set up params
  const params: DynamoDB.DocumentClient.UpdateItemInput = {
    TableName: process.env.PRODUCTS_TABLE!,
    Key: { PK: `PRODUCT#${productId}`, SK: "PRODUCT" },
    UpdateExpression: `SET ${updateExpressions.join(", ")}`,
    ExpressionAttributeNames: expressionAttributeNames,
    ExpressionAttributeValues: expressionAttributeValues,
    ReturnValues: "ALL_NEW"
  }


  try {
    const result: DynamoDB.DocumentClient.UpdateItemOutput = await dynamoDB.update(params).promise()
    const updatedProduct: Product = result.Attributes as Product

    return {
      statusCode: 200,
      body: JSON.stringify({
        success: true,
        data: updatedProduct
      })
    }
  } catch (error) {
    console.error("Error updating product:", error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Could not update product." }),
    };
  }

}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v. &lt;em&gt;Delete a product by ID handler (src/handlers/updateProduct.ts)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { APIGatewayProxyHandler } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import dynamoDB from "../utils/DynamoDbClient";

export const deleteProduct: APIGatewayProxyHandler = async (event)=&amp;gt; {
  const productId = event.pathParameters?.id

  if(!productId) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        success: false,
        error: 'Product ID is required'
      })
    }
  }

  const params: DynamoDB.DocumentClient.DeleteItemInput = {
    TableName: process.env.PRODUCTS_TABLE!,
    Key: {
      PK: `PRODUCT#${productId}`,
      SK: "PRODUCT",
    },
    ConditionExpression: "attribute_exists(PK)"
  }

  try {
    await dynamoDB.delete(params).promise()
    return {
      statusCode: 200,
      body: JSON.stringify({
        success: true,
        message: "Product deleted successfully"
      })
    }
  } catch (error: any) {
    console.error('Error deleting product:', error)

    if (error.code === "ConditionalCheckFailedException") {
      return {
        statusCode: 404,
        body: JSON.stringify({
          success: false,
          error: "Product not found.",
        }),
      };
    }

    return {
      statusCode: 500,
      body: JSON.stringify({
        success: false,
        error: "Could not delete product.",
      }),
    };
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7) &lt;strong&gt;Deploy Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can then proceed to deploy the service with completed code to AWS via the serverless.yml definitions using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serverless deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There might be need to prepend &lt;em&gt;sudo&lt;/em&gt; (for linux users) or add administrator access (for windows users) due to permission issues.&lt;/p&gt;

&lt;p&gt;This process provisions scalable Lambda functions/handlers to handle requests and responses, creates a Product DynamoDB table, and establishes an optimized architecture using CloudFormation.&lt;/p&gt;

&lt;p&gt;If successful we should get the following output, with the api-id and region being your personal api-id and AWS region in an example format shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;endpoints:
  POST - https://&amp;lt;api-id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev/products
  GET - https://&amp;lt;api-id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev/products
  GET - https://&amp;lt;api-id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev/products/{id}
  PUT - https://&amp;lt;api-id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev/products/{id}
  DELETE - https://&amp;lt;api-id&amp;gt;.execute-api.&amp;lt;region&amp;gt;.amazonaws.com/dev/products/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then proceed to test the endpoints gotten via a frontend client,in my case &lt;strong&gt;POSTMAN&lt;/strong&gt;, below are screenshots of the results&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To create a product&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjpsiei7syvqtlarvmcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjpsiei7syvqtlarvmcv.png" alt="Create a product" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Get all Products&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgt9e8m3wsydqlraqx22w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgt9e8m3wsydqlraqx22w.png" alt="To get all Products" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Get a Single product by ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftsqce3yhv0t9oare0ssk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftsqce3yhv0t9oare0ssk.png" alt="To Get a single product by Id" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Update a Single Product by ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo96p5ynolkvkl7tvkgrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo96p5ynolkvkl7tvkgrr.png" alt="Update Product by Id" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can then confirm if the product was truly updated by checking for all products once more, as we can see the product was updated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnuvqwslmc7azkogtaxr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnuvqwslmc7azkogtaxr.png" alt="Get all products-2" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To Delete a Single Product by ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi76gg1xury87xq21ses6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi76gg1xury87xq21ses6.png" alt="Delete-product-by-id" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can then proceed to make one more call to get this product and see if it exists, as we can see below the items count has reduced by one, indicating the product was indeed deleted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnajf5jwv04ts8pkn3j49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnajf5jwv04ts8pkn3j49.png" alt="get-all-products-3" width="800" height="500"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The ultimate test however, is to verify that our Lambda functions and DynamoDB table were successfully created and configured correctly. As shown below, both were successfully set up, yay! 😁🕺🏿&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdhqeaji6km86onsypqu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdhqeaji6km86onsypqu.png" alt="lambda" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibkhxb8srdlghivfcqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibkhxb8srdlghivfcqd.png" alt="DynamoDB" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In conclusion, we’ve successfully built serverless Lambda functions to perform CRUD operations and interact with a DynamoDB database using Node.js and TypeScript. While this serves as a foundational guide, there’s ample room to expand and enhance these concepts. This article however aims to provide a solid starting point for developers exploring the potential of serverless architecture and its practical applications.&lt;/p&gt;

&lt;p&gt;You can find the source code on &lt;a href="https://github.com/Geocoder89/serverless-app" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am also open to more questions and interactions on this and other technical coding subjects.&lt;/p&gt;

&lt;p&gt;Stay Learning, Happy Coding 🚀&lt;/p&gt;

&lt;p&gt;Follow me on LinkedIn &lt;a href="https://www.linkedin.com/in/oladele-omoarukhe/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
