<?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: Gloria</title>
    <description>The latest articles on DEV Community by Gloria (@boakye).</description>
    <link>https://dev.to/boakye</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%2F3837676%2F81d9daf9-ff19-47c9-bb9e-97857938a1d4.jpeg</url>
      <title>DEV Community: Gloria</title>
      <link>https://dev.to/boakye</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boakye"/>
    <language>en</language>
    <item>
      <title>Automated Integration Tests for the Deployed Hello World API</title>
      <dc:creator>Gloria</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:19:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/automated-integration-tests-for-the-deployed-hello-world-api-4fkk</link>
      <guid>https://dev.to/aws-builders/automated-integration-tests-for-the-deployed-hello-world-api-4fkk</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/aws-builders/build-your-first-aws-rest-api-the-smart-way-unit-integration-tests-live-docs-and-lnk"&gt;Part 1&lt;/a&gt;, we built our Hello World API and got it running locally. But writing code is only half the story — you also need to know it actually works, both before and after it leaves your machine.&lt;/p&gt;

&lt;p&gt;In this part, we will pick up where we left off. We will run local integration tests to catch issues early, deploy the API to AWS, verify the live deployment by hand with a browser and curl, and then add automated integration tests against the real deployed stack — so you are never relying on manual checks alone.&lt;/p&gt;

&lt;p&gt;Along the way, I share a few challenges I hit and how I worked through them, before wrapping up with final takeaways.&lt;/p&gt;

&lt;p&gt;By the end, you will have a full picture of the workflow across three stages — local integration testing, manual verification of the deployed API, and automated integration testing against the live AWS stack. There is also a &lt;strong&gt;hands-on challenge at the end&lt;/strong&gt;: &lt;/p&gt;

&lt;p&gt;-Add a new &lt;code&gt;/goodbye&lt;/code&gt; endpoint yourself and push it through all three test layers, from unit test to live deployment. That is your chance to practice the full workflow, not just read about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR — What We Built in Part 1
&lt;/h2&gt;

&lt;p&gt;In Part 1 we built a serverless REST API from scratch using AWS SAM, Python 3.11, and API Gateway. Here is what we shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A &lt;code&gt;template.yaml&lt;/code&gt; that defines all infrastructure as code — API Gateway, Lambda function, three routes, CORS, throttling, and CloudWatch metrics&lt;/li&gt;
&lt;li&gt;  A Lambda function (&lt;code&gt;app.py&lt;/code&gt;) with three endpoints: &lt;code&gt;GET /&lt;/code&gt;, &lt;code&gt;GET /hello&lt;/code&gt;, and &lt;code&gt;GET /get-documentation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  15 unit tests using pytest that call Lambda directly — no network, no Docker, no AWS
The unit tests all passed in 0.16 seconds. But, here is the question:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If your unit tests all pass, does that mean your API actually works?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not necessarily. And Part 2 is the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction — What Part 2 Covers
&lt;/h2&gt;

&lt;p&gt;Unit tests call your Lambda function directly in Python. They bypass API Gateway entirely. They never touch a network. They never check whether your &lt;code&gt;template.yaml&lt;/code&gt; wires the routes correctly, or whether the deployed stack responds the way a real client expects.&lt;/p&gt;

&lt;p&gt;Integration tests fix that. They go through the real path a request takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → API Gateway → Lambda → API Gateway → Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ✅ Understand what integration testing is and why unit tests are not enough&lt;/li&gt;
&lt;li&gt;  ✅ Learn the 2 types of integration tests this project implements&lt;/li&gt;
&lt;li&gt;  ✅ Write and run local integration tests against &lt;code&gt;sam local start-api&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  ✅ Deploy the API to AWS and manually verify all endpoints&lt;/li&gt;
&lt;li&gt;  ✅ Write and run deployed integration tests against your real AWS stack&lt;/li&gt;
&lt;li&gt;  ✅ Challenges and lessons learned&lt;/li&gt;
&lt;li&gt;  ✅ Hands-on challenge&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is Integration Testing?
&lt;/h2&gt;

&lt;p&gt;A unit test checks one small piece of your code in isolation. It calls a single function, passes in a fake input, and checks the output. Fast. No network. No infrastructure.&lt;/p&gt;

&lt;p&gt;An integration test checks how multiple pieces work &lt;strong&gt;together&lt;/strong&gt;. For a serverless API, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Does API Gateway route the request to the right Lambda function?&lt;/li&gt;
&lt;li&gt;  Do the CORS headers actually arrive at a real HTTP client?&lt;/li&gt;
&lt;li&gt;  What does a client receive when they hit a path that does not exist?&lt;/li&gt;
&lt;li&gt;  Does the deployed stack behave the same as the local simulation?
These questions cannot be answered by calling &lt;code&gt;lambda_handler()&lt;/code&gt; directly. You need a real HTTP request going through the full stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Unit tests prove your logic. Integration tests prove your wiring. Both are necessary. Neither replaces the other.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The 2 Types of Integration Tests This Project Implements
&lt;/h2&gt;

&lt;p&gt;This project implements integration testing at two levels, in a deliberate order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type 1 — Local integration tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Target: &lt;code&gt;sam local start-api&lt;/code&gt; running on &lt;code&gt;localhost:3000&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;SAM runs a Docker container that simulates Lambda locally and routes requests through a local API Gateway. Your tests hit &lt;code&gt;localhost:3000&lt;/code&gt; over real HTTP — no mocking, no shortcuts — but nothing leaves your machine and you pay nothing.&lt;/p&gt;

&lt;p&gt;Running these tests before deploying catches wiring problems early and gives you fast feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type 2 — Deployed integration tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Target: your real AWS stack after &lt;code&gt;sam deploy&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These tests fetch the live API Gateway URL from CloudFormation and send real HTTP requests to your deployed Lambda function in AWS. This is as close as you can get to what a real user experiences.&lt;/p&gt;

&lt;p&gt;Run these after deploying. They confirm your real AWS infrastructure works end-to-end.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Local integration&lt;/th&gt;
&lt;th&gt;Deployed integration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real AWS URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker needed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS account needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Tiny (pay-per-request Lambda)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it catches&lt;/td&gt;
&lt;td&gt;SAM wiring, routing, local behaviour&lt;/td&gt;
&lt;td&gt;Real deployment, real latency, real API Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;You need everything from Part 1, plus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;requests&lt;/code&gt; installed: &lt;code&gt;python -m pip install requests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;boto3&lt;/code&gt; installed: &lt;code&gt;python -m pip install boto3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Use &lt;code&gt;python -m pip install&lt;/code&gt; instead of just &lt;code&gt;pip install&lt;/code&gt;. This guarantees you are installing into the same Python environment that pytest uses. Using plain &lt;code&gt;pip&lt;/code&gt; can silently install into a different environment — your tests will then fail with &lt;code&gt;ModuleNotFoundError&lt;/code&gt; even though you think the package is installed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 1: Local Integration Tests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Integration Test Folder
&lt;/h3&gt;

&lt;p&gt;Your unit tests live in &lt;code&gt;tests/unit/&lt;/code&gt;. Integration tests get their own folder so you can run each layer independently:&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;mkdir &lt;/span&gt;hello-world-api/tests/integration
&lt;span class="nb"&gt;touch &lt;/span&gt;hello-world-api/tests/integration/__init__.py
&lt;span class="nb"&gt;touch &lt;/span&gt;hello-world-api/tests/integration/test_api_gateway_local.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your folder structure should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hello-world-api/
├── hello_world/
│   └── app.py
├── template.yaml
└── tests/
    ├── __init__.py
    ├── unit/
    │   ├── __init__.py
    │   └── test_app.py
    └── integration/
        ├── __init__.py
        └── test_api_gateway_local.py   ← we are building this now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the Local Integration Tests
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;tests/integration/test_api_gateway_local.py&lt;/code&gt; and paste in the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_default_name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_with_name_param&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gloria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Gloria!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_response_headers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_documentation_endpoint_returns_html&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/get-documentation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_path_returns_404&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/does-not-exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_post_request_rejected_by_api_gateway&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# API Gateway itself rejects this — your Lambda never runs
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the Test Code
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No mocking.&lt;/strong&gt; Compare this to your unit tests — there is no &lt;code&gt;mock_event()&lt;/code&gt; helper anywhere. There is nothing to mock. The request really travels over &lt;code&gt;localhost:3000&lt;/code&gt;, through SAM's local API Gateway simulation, into a Docker container running your Lambda function, and back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;BASE_URL = "http://localhost:3000"&lt;/code&gt;&lt;/strong&gt; is hardcoded. This is intentional for local tests because SAM Local runs the API on localhost:3000 by default, so using a constant keeps the test simple and easy to understand. When we move to deployed tests, the URL lives in AWS — not on your machine. Rather than hardcoding it, boto3 fetches it directly from CloudFormation, and the fixture delivers it to every test that needs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These tests will fail with a &lt;code&gt;ConnectionRefusedError&lt;/code&gt;&lt;/strong&gt; if &lt;code&gt;sam local start-api&lt;/code&gt; is not running. That is a feature, not a bug — it forces you to actually start the local API before the tests run, rather than silently testing nothing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The six tests and what each one proves:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;test_hello_default_name&lt;/code&gt; — sends &lt;code&gt;GET /hello&lt;/code&gt; with no parameters and checks that the response is &lt;code&gt;200&lt;/code&gt; and the message is &lt;code&gt;"Hello, World!"&lt;/code&gt;. This confirms the default name fallback works end-to-end through API Gateway, not just inside Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_hello_with_name_param&lt;/code&gt; — sends &lt;code&gt;GET /hello?name=Gloria&lt;/code&gt; and checks that the message becomes &lt;code&gt;"Hello, Gloria!"&lt;/code&gt;. This confirms query parameter extraction works through the full request path, not just in the unit test mock.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_hello_response_headers&lt;/code&gt; — checks that &lt;code&gt;Content-Type: application/json&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; actually arrive at the HTTP client. Your unit tests could assert these headers exist in Lambda's return value, but only an integration test confirms they survive the trip through API Gateway and arrive in the real HTTP response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_documentation_endpoint_returns_html&lt;/code&gt; — sends &lt;code&gt;GET /get-documentation&lt;/code&gt; and checks that the response is &lt;code&gt;200&lt;/code&gt;, the &lt;code&gt;Content-Type&lt;/code&gt; is &lt;code&gt;text/html&lt;/code&gt;, and the word &lt;code&gt;"Hello API"&lt;/code&gt; appears in the body. This confirms Lambda can serve HTML through API Gateway, not just JSON.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_unknown_path_returns_404&lt;/code&gt; — sends &lt;code&gt;GET /does-not-exist&lt;/code&gt; and checks for &lt;code&gt;404&lt;/code&gt; status code and &lt;code&gt;"Endpoint not found"&lt;/code&gt; in the error body. This feels natural coming from the unit tests — Lambda returns &lt;code&gt;404&lt;/code&gt; for unknown paths, so &lt;code&gt;404&lt;/code&gt; is what you expect. Write it this way first. It will fail, and that failure is the lesson. The full story is in the next section.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_post_request_rejected_by_api_gateway&lt;/code&gt; — sends &lt;code&gt;POST /hello&lt;/code&gt; and checks for &lt;code&gt;403&lt;/code&gt;. Only &lt;code&gt;GET&lt;/code&gt; is configured in &lt;code&gt;template.yaml&lt;/code&gt;, so API Gateway rejects the &lt;code&gt;POST&lt;/code&gt; before Lambda runs. A unit test calling &lt;code&gt;lambda_handler()&lt;/code&gt; directly would never see this &lt;code&gt;403&lt;/code&gt; because Lambda was never invoked.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Local Integration Tests
&lt;/h3&gt;

&lt;p&gt;You need two terminals open at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal 1 — start the local API:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Mounting HelloWorldFunction at http://127.0.0.1:3000/ &lt;span class="o"&gt;[&lt;/span&gt;GET]
Mounting HelloWorldFunction at http://127.0.0.1:3000/get-documentation &lt;span class="o"&gt;[&lt;/span&gt;GET]
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello &lt;span class="o"&gt;[&lt;/span&gt;GET]
You can now browse to the above endpoints to invoke your functions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terminal 2 — run the tests:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/integration/test_api_gateway_local.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What You Should See — and One Surprise
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tests/integration/test_api_gateway_local.py::test_hello_default_name PASSED &lt;span class="o"&gt;[&lt;/span&gt; 16%]
tests/integration/test_api_gateway_local.py::test_hello_with_name_param PASSED &lt;span class="o"&gt;[&lt;/span&gt; 33%]
tests/integration/test_api_gateway_local.py::test_hello_response_headers PASSED &lt;span class="o"&gt;[&lt;/span&gt; 50%]
tests/integration/test_api_gateway_local.py::test_documentation_endpoint_returns_html PASSED &lt;span class="o"&gt;[&lt;/span&gt; 66%]
tests/integration/test_api_gateway_local.py::test_unknown_path_returns_404 FAILED &lt;span class="o"&gt;[&lt;/span&gt; 83%]
tests/integration/test_api_gateway_local.py::test_post_request_rejected_by_api_gateway PASSED &lt;span class="o"&gt;[&lt;/span&gt;100%]

1 failed, 5 passed &lt;span class="k"&gt;in &lt;/span&gt;11.73s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One test failed. Here is the full error:&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="o"&gt;==================================&lt;/span&gt; FAILURES &lt;span class="o"&gt;===================================&lt;/span&gt;
________________________ test_unknown_path_returns_404 ________________________

    def test_unknown_path_returns_404&lt;span class="o"&gt;()&lt;/span&gt;:
        response &lt;span class="o"&gt;=&lt;/span&gt; requests.get&lt;span class="o"&gt;(&lt;/span&gt;f&lt;span class="s2"&gt;"{BASE_URL}/does-not-exist"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;       assert response.status_code &lt;span class="o"&gt;==&lt;/span&gt; 404
E       assert 403 &lt;span class="o"&gt;==&lt;/span&gt; 404
E        +  where 403 &lt;span class="o"&gt;=&lt;/span&gt; &amp;lt;Response &lt;span class="o"&gt;[&lt;/span&gt;403]&amp;gt;.status_code

tests&lt;span class="se"&gt;\i&lt;/span&gt;ntegration&lt;span class="se"&gt;\t&lt;/span&gt;est_api_gateway_local.py:34: AssertionError
&lt;span class="o"&gt;===========================&lt;/span&gt; short &lt;span class="nb"&gt;test &lt;/span&gt;summary info &lt;span class="o"&gt;===========================&lt;/span&gt;
FAILED tests/integration/test_api_gateway_local.py::test_unknown_path_returns_404

&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%2Fpuev5uebbmj55lu3zrm0.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%2Fpuev5uebbmj55lu3zrm0.png" alt=" " width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discovery #1: Why 403 and Not 404?
&lt;/h3&gt;

&lt;p&gt;Your first instinct might be to think you broke something. You did not. This is API Gateway teaching you something a unit test never could.&lt;/p&gt;

&lt;p&gt;Here is what actually happens when you request &lt;code&gt;/does-not-exist&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The request hits API Gateway first.&lt;/li&gt;
&lt;li&gt;API Gateway checks &lt;code&gt;template.yaml&lt;/code&gt; for a matching route.&lt;/li&gt;
&lt;li&gt;No matching route exists.&lt;/li&gt;
&lt;li&gt;API Gateway rejects the request itself, with &lt;code&gt;403 Missing Authentication Token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Lambda never runs.
Compare that to your unit test, which calls &lt;code&gt;lambda_handler()&lt;/code&gt; directly — skipping API Gateway entirely. Lambda runs, hits &lt;code&gt;return error_response(404, "Endpoint not found")&lt;/code&gt;, and returns a clean &lt;code&gt;404&lt;/code&gt;. That is correct Lambda behaviour. It is just not what a real client ever sees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gap — between what your code does and what a client actually receives — is exactly what integration tests are for. Unit tests cannot catch it, because they never involve API Gateway at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt; — update the test to match the real behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_path_returns_403&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/does-not-exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing Authentication Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run again after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;6 passed &lt;span class="k"&gt;in &lt;/span&gt;10.54s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit test still correctly asserts &lt;code&gt;404&lt;/code&gt; — that is Lambda's behaviour when called directly. This integration test correctly asserts &lt;code&gt;403&lt;/code&gt; — that is API Gateway's behaviour when a real request goes through the full stack. Both are right. They are just testing different layers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In this API, &lt;code&gt;403 Missing Authentication Token&lt;/code&gt; on an unknown path means API Gateway could not match the request to a configured resource or method. It does not mean Lambda rejected the request or that your Lambda execution role lacks permission.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember this:&lt;/strong&gt; whenever you see &lt;code&gt;403 Missing Authentication Token&lt;/code&gt; from API Gateway, check the route and HTTP method before assuming you have an IAM problem.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 2: Deploy the API to AWS
&lt;/h2&gt;

&lt;p&gt;Before we can run deployed integration tests, we need a deployed stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Build and Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam build
sam deploy &lt;span class="nt"&gt;--guided&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts. When asked for the stack name, enter &lt;code&gt;hello-world-api&lt;/code&gt;. Accept the defaults for everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Setting default arguments &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'sam deploy'&lt;/span&gt;
Stack Name &lt;span class="o"&gt;[&lt;/span&gt;sam-app]: hello-world-api
AWS Region &lt;span class="o"&gt;[&lt;/span&gt;us-east-1]:
&lt;span class="c"&gt;#Shows you resources changes to be deployed and require a 'Y' to initiate deploy&lt;/span&gt;
Confirm changes before deploy &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
&lt;span class="c"&gt;#SAM needs permission to be able to create roles to connect to the resources in your template&lt;/span&gt;
Allow SAM CLI IAM role creation &lt;span class="o"&gt;[&lt;/span&gt;Y/n]:
&lt;span class="c"&gt;#Preserves the state of previously provisioned resources when an operation fails&lt;/span&gt;
Disable rollback &lt;span class="o"&gt;[&lt;/span&gt;y/N]:
HelloWorldFunction has no authentication. Is this okay? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
HelloWorldFunction has no authentication. Is this okay? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
HelloWorldFunction has no authentication. Is this okay? &lt;span class="o"&gt;[&lt;/span&gt;y/N]: y
Save arguments to configuration file &lt;span class="o"&gt;[&lt;/span&gt;Y/n]: y
SAM configuration file &lt;span class="o"&gt;[&lt;/span&gt;samconfig.toml]:
SAM configuration environment &lt;span class="o"&gt;[&lt;/span&gt;default]:
&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%2Fdtuvnrqupsv887zbpcp0.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%2Fdtuvnrqupsv887zbpcp0.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the end of a successful deployment, CloudFormation prints your stack outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CloudFormation outputs from deployed stack
&lt;span class="nt"&gt;---------------------------------------------------------------------------&lt;/span&gt;
Key                 HelloApiUrl
Description         Hello endpoint
Value               https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello

Key                 RootUrl
Description         Root URL - serves documentation page
Value               https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/

Key                 DocumentationUrl
Description         Documentation endpoint
Value               https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/get-documentation
&lt;span class="nt"&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%2F8cdbh19idikx928qfg0s.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%2F8cdbh19idikx928qfg0s.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The string &lt;code&gt;azmimofsv1&lt;/code&gt; in the URL is your API Gateway ID. AWS generates it randomly — yours will be different. Replace it with your own ID in all the commands below.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 3: Manually Test the Deployed API Endpoints
&lt;/h2&gt;

&lt;p&gt;Before running any automated tests, verify the deployed endpoints are alive and responding correctly. Do not trust automation to confirm something works until you have seen it work yourself, at least once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Using the Browser
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;GET /hello&lt;/code&gt; — default greeting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paste your &lt;code&gt;HelloApiUrl&lt;/code&gt; into the browser address bar. You should see:&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-20T22:48:21.123456+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&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;&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%2Fgf2rvtmq64wxqr06su2z.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%2Fgf2rvtmq64wxqr06su2z.png" alt=" " width="798" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;GET /hello&lt;/code&gt; with a name parameter:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?name=YourName&lt;/code&gt; to the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello?name=Gloria
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, Gloria!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-01T11:58:58.392697+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&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;&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%2Fsevcf8mwymckvxg2onum.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%2Fsevcf8mwymckvxg2onum.png" alt=" " width="798" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;GET /get-documentation&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paste your &lt;code&gt;DocumentationUrl&lt;/code&gt; into a new browser tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/get-documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the styled HTML documentation page served directly from AWS Lambda.&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%2F5pfty3h0zpqrazzrt7ny.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%2F5pfty3h0zpqrazzrt7ny.png" alt=" " width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;GET /&lt;/code&gt; — root endpoint:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Paste your &lt;code&gt;RootUrl&lt;/code&gt; into a new tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should also render the documentation page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using curl
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test /hello&lt;/span&gt;
curl https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello

&lt;span class="c"&gt;# Test /hello with name&lt;/span&gt;
curl https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello?name&lt;span class="o"&gt;=&lt;/span&gt;Gloria

&lt;span class="c"&gt;# Test /get-documentation&lt;/span&gt;
curl https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/get-documentation

&lt;span class="c"&gt;# Test root&lt;/span&gt;
curl https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/
&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%2Frsl4s9pagxn8q6vnoj01.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%2Frsl4s9pagxn8q6vnoj01.png" alt=" " width="799" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test non-GET methods — should be blocked by API Gateway:&lt;/strong&gt;&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="c"&gt;# POST — should return 403&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello

&lt;span class="c"&gt;# PUT — should return 403&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello
&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%2Frdrfefsye6wn0f4mye3i.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%2Frdrfefsye6wn0f4mye3i.png" alt=" " width="800" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test an unknown path:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/does-not-exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;{"message": "Missing Authentication Token"}&lt;/code&gt; with a &lt;code&gt;403&lt;/code&gt; status code. Your Lambda never ran. This is the same &lt;code&gt;403&lt;/code&gt; behaviour you discovered during local integration testing — now confirmed against the real deployed stack.&lt;/p&gt;

&lt;p&gt;All four GET endpoints responding correctly. Good. Now we automate all of this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Automated Integration Tests for the Deployed Hello World API
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Deployed Integration Test File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;hello-world-api/tests/integration/test_api_gateway_deployed.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your folder structure should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hello-world-api/
├── hello_world/
│   └── app.py
├── template.yaml
└── tests/
    ├── __init__.py
    ├── unit/
    │   ├── __init__.py
    │   └── test_app.py
    └── integration/
        ├── __init__.py
        ├── test_api_gateway_local.py
        └── test_api_gateway_deployed.py  ← we are building this now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the Deployed Integration Tests
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;tests/integration/test_api_gateway_deployed.py&lt;/code&gt; and paste in the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test.
Run this before pytest:
    export AWS_SAM_STACK_NAME=hello-world-api
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="c1"&gt;# ── fixtures ──────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch all API URLs from the deployed CloudFormation stack outputs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;stack_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_SAM_STACK_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stack_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please set the AWS_SAM_STACK_NAME environment variable.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run: export AWS_SAM_STACK_NAME=hello-world-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudformation&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_stacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot find stack &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Make sure you have run &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sam deploy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; before running these tests.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;

    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Outputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Build a dictionary of all stack outputs keyed by OutputKey
&lt;/span&gt;    &lt;span class="n"&gt;url_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OutputKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OutputValue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Confirm the keys we need are actually present
&lt;/span&gt;    &lt;span class="n"&gt;required_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RootUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected output key &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not found in stack &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url_map&lt;/span&gt;



&lt;span class="c1"&gt;# ── tests ─────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_default_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_with_name_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gloria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Gloria!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_response_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_documentation_endpoint_returns_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_root_endpoint_returns_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RootUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_path_returns_403&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Extract base URL from HelloApiUrl
&lt;/span&gt;    &lt;span class="c1"&gt;# HelloApiUrl = https://xxx.execute-api.us-east-1.amazonaws.com/Prod/hello
&lt;/span&gt;    &lt;span class="c1"&gt;# Base URL    = https://xxx.execute-api.us-east-1.amazonaws.com/Prod
&lt;/span&gt;    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/does-not-exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# API Gateway returns 403 for unknown paths — Lambda is never invoked
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_post_request_rejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# API Gateway rejects POST — Lambda is never invoked
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the Test Code
&lt;/h3&gt;

&lt;p&gt;The file has two parts: one fixture that uses boto3 to fetch the URLs from CloudFormation and delivers them to the tests, and seven tests that use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imports and the environment variable&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;os&lt;/code&gt;: reads the shell environment variable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;boto3&lt;/code&gt;: talks to AWS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requests&lt;/code&gt;: makes the HTTP calls.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;pytest&lt;/code&gt;: provides the fixture system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fixtures&lt;/strong&gt;&lt;br&gt;
The first thing the fixture does is read your stack name from the shell:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_SAM_STACK_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stack_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please set the AWS_SAM_STACK_NAME environment variable.
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run: export AWS_SAM_STACK_NAME=hello-world-api&lt;/span&gt;&lt;span class="sh"&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 stack name is not hardcoded in the file. It lives in a shell environment variable you set before running the tests. This keeps it out of your code so the same test file works for any stack name, any environment. The &lt;code&gt;if stack_name is None&lt;/code&gt; check gives you a clear, actionable error if you forget to set it, instead of a confusing boto3 exception later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;@pytest.fixture()&lt;/code&gt; decorator&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decorator transforms &lt;code&gt;stack_urls&lt;/code&gt; from a plain function into a pytest fixture — a piece of setup code that pytest runs automatically and injects into any test that lists &lt;code&gt;stack_urls&lt;/code&gt; as a parameter. Pytest sees the parameter name in a test, finds the matching fixture, runs it, and passes the return value in.&lt;/p&gt;

&lt;p&gt;The reason this is a fixture rather than a constant like &lt;code&gt;BASE_URL = "http://localhost:3000"&lt;/code&gt; is that we do not want to hardcode environment-specific AWS URLs into our tests. The API Gateway URL is tied to the deployed API and stage, and we can retrieve the current value directly from CloudFormation. This makes the tests portable across environments and avoids manually updating URLs if the stack configuration changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;boto3 fetches the URL from CloudFormation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudformation&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_stacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot find stack &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Make sure you have run &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sam deploy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; before running these tests.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;sam deploy&lt;/code&gt; completes, CloudFormation stores your stack output values — including all three API URLs you defined in &lt;code&gt;template.yaml&lt;/code&gt;. &lt;code&gt;boto3&lt;/code&gt; reads those outputs here with &lt;code&gt;describe_stacks&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;try/except&lt;/code&gt; block gives you a clear error message if the stack does not exist yet — which happens if you run the tests before deploying. Without it, you would get a raw boto3 exception that does not tell you what to do next.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building the dictionary&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stacks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Outputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;url_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OutputKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OutputValue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;required_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RootUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected output key &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not found in stack &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stack_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url_map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;outputs&lt;/code&gt; is a list of dictionaries from CloudFormation — one per output key. The list comprehension converts it into a single flat dictionary:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/get-documentation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RootUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://azmimofsv1.execute-api.us-east-1.amazonaws.com/Prod/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloWorldFunctionArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:lambda:...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloWorldFunctionIamRole&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam:...&lt;/span&gt;&lt;span class="sh"&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 fixture returns this whole dictionary rather than a single URL. The &lt;code&gt;required_keys&lt;/code&gt; check fails fast with a clear message if your stack is missing an expected output key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The seven tests&lt;/strong&gt;&lt;br&gt;
Every test receives &lt;code&gt;stack_urls&lt;/code&gt; as a parameter and uses it to get the URL it needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;test_hello_default_name&lt;/code&gt; — sends &lt;code&gt;GET /hello&lt;/code&gt; with no parameters and checks that the status is &lt;code&gt;200&lt;/code&gt;, the message is &lt;code&gt;"Hello, World!"&lt;/code&gt;, and the version is &lt;code&gt;"1.0"&lt;/code&gt;. This confirms the full deployed stack — API Gateway, Lambda, routing, and response formatting — is wired correctly end to end.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_hello_with_name_param&lt;/code&gt; — sends &lt;code&gt;GET /hello?name=Gloria&lt;/code&gt; and checks that the message becomes &lt;code&gt;"Hello, Gloria!"&lt;/code&gt;. This confirms query parameter extraction works through the real deployed API Gateway, not just locally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_hello_response_headers&lt;/code&gt; — checks that &lt;code&gt;Content-Type&lt;/code&gt; contains &lt;code&gt;application/json&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; is &lt;code&gt;*&lt;/code&gt;. Notice the assertion uses &lt;code&gt;in&lt;/code&gt; instead of &lt;code&gt;==&lt;/code&gt; for &lt;code&gt;Content-Type&lt;/code&gt;. The deployed API Gateway adds &lt;code&gt;charset=utf-8&lt;/code&gt; — returning &lt;code&gt;application/json; charset=utf-8&lt;/code&gt; — that &lt;code&gt;sam local&lt;/code&gt; does not. Using &lt;code&gt;in&lt;/code&gt; means this test passes in both environments without any changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_documentation_endpoint_returns_html&lt;/code&gt; — sends &lt;code&gt;GET /get-documentation&lt;/code&gt; and checks for &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;text/html&lt;/code&gt; content type, and &lt;code&gt;"Hello API"&lt;/code&gt; in the body. This confirms the HTML documentation endpoint works in the real deployed stack, served from Lambda through API Gateway.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_root_endpoint_returns_html&lt;/code&gt; — sends &lt;code&gt;GET /&lt;/code&gt; and checks for &lt;code&gt;200&lt;/code&gt; and &lt;code&gt;text/html&lt;/code&gt;. This confirms the root endpoint is correctly routed to the same documentation handler as &lt;code&gt;/get-documentation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_unknown_path_returns_403&lt;/code&gt; — there is no &lt;code&gt;BaseUrl&lt;/code&gt; output key in your CloudFormation stack, so the test derives the base URL by stripping &lt;code&gt;/hello&lt;/code&gt; from the end of &lt;code&gt;HelloApiUrl&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/does-not-exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a small but deliberate choice — rather than adding another output to &lt;code&gt;template.yaml&lt;/code&gt;, you compute what you need from what you already have.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;test_post_request_rejected&lt;/code&gt; — sends &lt;code&gt;POST /hello&lt;/code&gt; and checks for &lt;code&gt;403&lt;/code&gt;. Confirms that the deployed API Gateway rejects non-GET methods the same way the local simulation does. Lambda is never invoked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Set the Environment Variable
&lt;/h3&gt;

&lt;p&gt;The test reads your stack name from a shell environment variable — not from a file inside your project. This keeps your stack name out of your code, meaning the same test file works for any environment by changing one variable.&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;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SAM_STACK_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;hello-world-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This variable lives in your terminal session only. It disappears when you close the terminal. Set it again each time you open a new session before running deployed tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Run the Deployed Integration Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/integration/test_api_gateway_deployed.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What You Should See
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tests/integration/test_api_gateway_deployed.py::test_hello_default_name PASSED
tests/integration/test_api_gateway_deployed.py::test_hello_with_name_param PASSED
tests/integration/test_api_gateway_deployed.py::test_hello_response_headers PASSED
tests/integration/test_api_gateway_deployed.py::test_documentation_endpoint_returns_html PASSED
tests/integration/test_api_gateway_deployed.py::test_root_endpoint_returns_html PASSED
tests/integration/test_api_gateway_deployed.py::test_unknown_path_returns_403 PASSED
tests/integration/test_api_gateway_deployed.py::test_post_request_rejected PASSED

7 passed &lt;span class="k"&gt;in &lt;/span&gt;21.25s
&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%2Fevh8guo1tadgptafbw9a.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%2Fevh8guo1tadgptafbw9a.png" alt=" " width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;7 passed means your deployed stack is healthy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why 21 Seconds?
&lt;/h3&gt;

&lt;p&gt;Your local integration tests ran in 11 seconds. Your deployed tests took 21 seconds. The difference comes from testing real infrastructure across the network, plus the additional processing involved in API Gateway and Lambda. That extra time is expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discovery #2 — Empty Query Parameter Bug
&lt;/h2&gt;

&lt;p&gt;Seven tests passed. I was almost ready to publish. Then I tested one more edge case.&lt;/p&gt;

&lt;p&gt;I sent this request to the deployed API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;/hello?name=
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response came back as:&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="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, !"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-15T22:17:33.974145+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&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;&lt;code&gt;"Hello, !"&lt;/code&gt; — the name is empty. The intended behaviour was &lt;code&gt;"Hello, World!"&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Could a unit test have caught this?&lt;/strong&gt; Yes. If we had written a unit test for the specific case of an empty string, it could have caught the bug. The problem was not that unit testing was incapable of finding it. We simply had not thought to test this edge case. The deployed integration test gave me another opportunity to discover it because I was testing the API with a real HTTP request and real query-string input — like what happens when a user sends &lt;code&gt;?name=&lt;/code&gt; with nothing after the equals sign.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a real application bug, not an infrastructure problem. And I only found it because I was testing edge cases before publishing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Happened
&lt;/h3&gt;

&lt;p&gt;The original Lambda code uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&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 only falls back to &lt;code&gt;"World"&lt;/code&gt; when the &lt;code&gt;name&lt;/code&gt; key is completely absent from the query parameters. It handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;/hello&lt;/code&gt; — no &lt;code&gt;name&lt;/code&gt; key → falls back to &lt;code&gt;"World"&lt;/code&gt; ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it does not handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;/hello?name=&lt;/code&gt; — &lt;code&gt;name&lt;/code&gt; key exists, value is an empty string → returns &lt;code&gt;""&lt;/code&gt; ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is present in the dictionary, so &lt;code&gt;.get()&lt;/code&gt; returns the empty string instead of the default.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix — Adding a Regression Test
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; First, write a test that exposes the bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_with_empty_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;stack_urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&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;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Run it — it fails. That is the bug confirmed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Update the Lambda handler. Change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;or&lt;/code&gt; operator returns &lt;code&gt;"World"&lt;/code&gt; when &lt;code&gt;name&lt;/code&gt; is either absent (&lt;code&gt;None&lt;/code&gt;) or an empty string (&lt;code&gt;""&lt;/code&gt;). Both cases now fall back to the default correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Add the regression test above to your deployed integration test file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; Redeploy with &lt;code&gt;sam build&lt;/code&gt; and &lt;code&gt;sam deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6:&lt;/strong&gt; Run the full test suite again and confirm all tests pass.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once you apply the fix, keep this test in your suite permanently. It is now a &lt;strong&gt;regression test&lt;/strong&gt; — its job is to make sure this specific bug never comes back. If someone later changes the Lambda handler and accidentally reintroduces the old &lt;code&gt;query_params.get("name", "World")&lt;/code&gt; behaviour, this test fails immediately and catches it before it ships. Every bug you fix is an opportunity to write a regression test. Over time, your test suite becomes a record of every mistake you caught and fixed.&lt;/p&gt;

&lt;p&gt;That is the real workflow in practice:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover
   ↓
Write a failing test
   ↓
Fix the code
   ↓
Deploy
   ↓
Run the tests
   ↓
Confirm everything passes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this discovery taught me&lt;/strong&gt;: integration testing does not just verify infrastructure. It can also expose application behaviour bugs when we exercise the system with realistic inputs. This bug could have been caught by a unit test, but my existing unit tests did not include the empty-string case. Testing the deployed API with realistic inputs exposed application behaviour bugs that I did not think to cover in my original automated tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is one of the reasons I now see testing as more than simply checking whether the code works. Different tests give you different opportunities to discover problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Unit tests verify individual pieces of application logic.&lt;/li&gt;
&lt;li&gt;  Local integration tests verify how those pieces work together through the local HTTP interface.&lt;/li&gt;
&lt;li&gt;  Deployed integration tests exercise the application through the real API Gateway, Lambda, and AWS infrastructure.&lt;/li&gt;
&lt;li&gt;  Manual verification gives you another opportunity to try realistic requests and edge cases before publishing.
&amp;gt; &lt;em&gt;The goal is not to choose one type of test. It is to build enough layers of testing that a bug has fewer places to hide.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running All Three Layers Together
&lt;/h2&gt;

&lt;p&gt;You now have all three test layers. Here is how to run each one:&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="c"&gt;# Unit tests — fastest, no Docker, no AWS needed&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/unit/ &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# Local integration — Docker needed, sam local start-api must be running&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/integration/test_api_gateway_local.py &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# Deployed integration — AWS deployment needed, set env variable first&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SAM_STACK_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;hello-world-api
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/integration/test_api_gateway_deployed.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your complete results across all layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tests/unit/test_app.py                                  15 passed    0.16s
tests/integration/test_api_gateway_local.py              6 passed   11.53s
tests/integration/test_api_gateway_deployed.py           7 passed   21.25s

28 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;28 tests across three layers!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you followed Discovery #2 and applied the fix&lt;/strong&gt;, your test counts will differ from the ones shown above. To properly cover the fix, you would add an empty-name test to your deployed integration test file, a matching test to your local integration test file, and a unit test for the fixed behaviour. This bring the totals to 16 unit tests, 7 local integration tests, and 8 deployed integration tests, for a total of 31 passing tests. That is a good thing. Every bug you find and add a test for makes your test suite stronger. Real test suites grow as you discover and fix real bugs. That is exactly what happened here.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 403 vs 404 mismatch&lt;/strong&gt; This one was subtler. My unit tests returned &lt;code&gt;404&lt;/code&gt; for unknown paths, my integration tests returned &lt;code&gt;403&lt;/code&gt; for unknown path, and both were "correct" — just testing different layers. The real lesson: API Gateway rejects unmatched routes before Lambda ever runs, so a unit test that calls &lt;code&gt;lambda_handler()&lt;/code&gt; directly can never see that behavior. If you hit this, don't assume your code is broken — check whether the layer you're testing even reaches API Gateway or not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The empty-string edge case&lt;/strong&gt; Seven tests passed, and I was ready to publish but, then I tried &lt;code&gt;/hello?name=&lt;/code&gt; just to be sure, and got back &lt;code&gt;"Hello, !"&lt;/code&gt; instead of &lt;code&gt;"Hello, World!"&lt;/code&gt;. The bug was one line: &lt;code&gt;query_params.get("name", "World")&lt;/code&gt; only falls back to the default when name is missing entirely, not when it's present but empty. A unit test could have caught it. I just hadn't thought to write one for that case. Good reminder that passing tests only prove what you actually tested for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Understanding &lt;code&gt;@pytest.fixture()&lt;/code&gt;&lt;/strong&gt; took time. The decorator pattern — where a function is registered by name and its return value is injected into tests automatically — is not intuitive if you are used to calling functions directly. The key insight: by the time the fixture value arrives in your test, it is no longer a function. It is just a value — whatever the fixture returned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;python -m pip install&lt;/code&gt; vs &lt;code&gt;pip install&lt;/code&gt; distinction&lt;/strong&gt; caused a real failure. The error was &lt;code&gt;ModuleNotFoundError: No module named 'requests'&lt;/code&gt; even after installing it. The Windows Store Python creates a separate environment that &lt;code&gt;pip&lt;/code&gt; alone does not always target. Using &lt;code&gt;python -m pip&lt;/code&gt; fixes it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Content-Type&lt;/code&gt; header difference&lt;/strong&gt; between &lt;code&gt;sam local&lt;/code&gt; and the real API Gateway surprised me. Local returns &lt;code&gt;application/json&lt;/code&gt;. Deployed returns &lt;code&gt;application/json; charset=utf-8&lt;/code&gt;. Using &lt;code&gt;in&lt;/code&gt; instead of &lt;code&gt;==&lt;/code&gt; in the assertion handles both environments without duplicating tests.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unit tests prove your logic. Integration tests prove your wiring.&lt;/strong&gt; A unit test that passes and an integration test that fails means your code is correct but your infrastructure is broken. Both layers are necessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;403 Missing Authentication Token&lt;/code&gt; usually means a missing route, not a permissions problem.&lt;/strong&gt; API Gateway returns this for any path not configured in &lt;code&gt;template.yaml&lt;/code&gt;. Your Lambda's &lt;code&gt;404&lt;/code&gt; only appears if Lambda actually runs — for unknown paths, it never does.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Passing tests only prove what you thought to test.&lt;/strong&gt; 28 green tests felt like proof the API was solid. The empty-string bug was sitting there the whole time, untouched by any of those 28 checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Different test layers give you different chances to catch the same bug.&lt;/strong&gt; The empty-string edge case could have been caught by a unit test, but it was not. It took a real HTTP request with a realistic, slightly unusual input to surface it. Testing the deployed API is not just about checking infrastructure — it is another opportunity to probe behaviour with inputs a real user might actually send.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never trust automation before you have verified manually.&lt;/strong&gt; Running curl against your deployed URL before writing integration tests is not optional. It is how you confirm the deployment worked before asking a test suite to confirm it for you.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You've now tested this API three different ways: unit tests against the raw Lambda handler, integration tests against the local API Gateway emulator, and integration tests against the real, deployed stack — 28 passing tests total, proving the API actually works, not just that it runs.&lt;/p&gt;

&lt;p&gt;More importantly, you now know &lt;em&gt;why&lt;/em&gt; each layer exists. The 403 vs 404 discovery showed that API Gateway rejects unknown paths before Lambda ever runs. Your unit tests could never catch that because they call Lambda directly and bypass API Gateway entirely. Only a real integration test — one that sends an actual HTTP request through the full stack — could expose it.&lt;/p&gt;

&lt;p&gt;Then there's the empty-string bug: &lt;code&gt;/hello?name=&lt;/code&gt; returned &lt;code&gt;"Hello, !"&lt;/code&gt; instead of &lt;code&gt;"Hello, World!"&lt;/code&gt;, even with seven deployed tests all green. Nothing infrastructural was wrong. I just hadn't thought to test that specific case. It's now a permanent regression test, and a reminder that "all green" is a checkpoint, not a finish line&lt;/p&gt;

&lt;p&gt;The biggest takeaway isn't the tests themselves. Passing the planned tests does not necessarily mean the application is ready. Continuing to test with different and unexpected inputs can reveal behavior that the original test cases did not cover.&lt;/p&gt;




&lt;h2&gt;
  
  
  Call to Action — Your Turn
&lt;/h2&gt;

&lt;p&gt;You have 28 passing tests. Now break something on purpose and fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a new endpoint to the API:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Add a &lt;code&gt;GET /goodbye&lt;/code&gt; endpoint to &lt;code&gt;app.py&lt;/code&gt; that returns &lt;code&gt;{"message": "Goodbye, {name}!"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Add the route to &lt;code&gt;template.yaml&lt;/code&gt; under &lt;code&gt;Events&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Test it manually with &lt;code&gt;sam local start-api&lt;/code&gt; and curl&lt;/li&gt;
&lt;li&gt; Add a unit test for it in &lt;code&gt;test_app.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Add an integration test for it in &lt;code&gt;test_api_gateway_local.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Deploy with &lt;code&gt;sam deploy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Test the deployed endpoint with curl&lt;/li&gt;
&lt;li&gt; Add a deployed integration test for it in &lt;code&gt;test_api_gateway_deployed.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Run all 3 test layers and confirm everything passes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try it, drop a comment with your result — I would love to see what you build.&lt;/p&gt;

&lt;p&gt;Connect with me on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/gloriaboakye/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@gloriaboakye" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Globak143" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>api</category>
      <category>testing</category>
      <category>python</category>
    </item>
    <item>
      <title>Build Your First AWS REST API the Smart Way — Unit &amp; Integration Tests, Live Docs, and Infrastructure as Code</title>
      <dc:creator>Gloria</dc:creator>
      <pubDate>Tue, 07 Jul 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/build-your-first-aws-rest-api-the-smart-way-unit-integration-tests-live-docs-and-lnk</link>
      <guid>https://dev.to/aws-builders/build-your-first-aws-rest-api-the-smart-way-unit-integration-tests-live-docs-and-lnk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;APIs are the foundation of modern software development, which is why this "Hello World!" API is anything but basic. &lt;/p&gt;

&lt;p&gt;Most tutorials rush past the foundational concepts, pushing you to build complex systems before you truly understand how everything connects. This guide is different. &lt;br&gt;
By the end, you won't just have a working API—you will understand exactly what is happening under the hood. More importantly, you will have a production-ready infrastructure that you can share, repeat, and version-control. This is the exact API that started my cloud journey. Building it gave me the foundational knowledge to later &lt;a href="https://medium.com/@gloriaboakye/how-i-built-an-aws-api-to-automate-school-data-tracking-for-165-schools-and-saved-25-hours-per-d349803de953" rel="noopener noreferrer"&gt;build a production serverless API that automates data tracking for 165 schools and saves me 25+ hours a year.&lt;/a&gt; &lt;br&gt;
Looking back at that project, my biggest takeaway was realizing why we test production APIs using automated code rather than just console testing. This tutorial is me putting that vital lesson into practice. &lt;br&gt;
By building this Hello World API, you will master the core fundamentals that power real-world production systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ✅ HTTP request handling&lt;/li&gt;
&lt;li&gt;  ✅ Query parameter extraction&lt;/li&gt;
&lt;li&gt;  ✅ JSON response formatting&lt;/li&gt;
&lt;li&gt;  ✅ CORS configuration&lt;/li&gt;
&lt;li&gt;  ✅ Serverless deployment&lt;/li&gt;
&lt;li&gt;  ✅ Infrastructure as Code&lt;/li&gt;
&lt;li&gt;  ✅ Unit testing&lt;/li&gt;
&lt;li&gt;  ✅ Integration testing&lt;/li&gt;
&lt;li&gt;  ✅ Deployment to AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The pattern you will learn here — &lt;strong&gt;receive request → process data → return response&lt;/strong&gt; — is the exact same blueprint used in every enterprise API, from simple microservices to complex distributed systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every complex system starts right here. Master this fundamental pattern, and you can build anything.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Smarter Way to Build Your First API
&lt;/h3&gt;

&lt;p&gt;The AWS Management Console is a great place to start building and exploring APIs. Clicking through Lambda, API Gateway, and IAM helps you see exactly what AWS is creating under the hood. But here’s the thing: once you understand what each service does, manually clicking through the console becomes a major bottleneck:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-consuming&lt;/strong&gt;: Taking 30+ minutes just to wire up a simple endpoint.&lt;br&gt;
&lt;strong&gt;Error-prone&lt;/strong&gt;: Forgetting critical settings when recreating the setup.&lt;br&gt;
&lt;strong&gt;Unshareable&lt;/strong&gt;: Preventing teammates from reliably reproducing your exact environment.&lt;br&gt;
&lt;strong&gt;Untracked&lt;/strong&gt;: Lacking version control, deployment history, or easy rollback.&lt;/p&gt;

&lt;p&gt;That’s exactly why we’re using AWS SAM.&lt;/p&gt;


&lt;h3&gt;
  
  
  What is AWS SAM (Serverless Application Model)?
&lt;/h3&gt;

&lt;p&gt;AWS SAM lets you define your entire API infrastructure in a single YAML file and deploy it with one command. No clicking through menus. No guesswork. Just clean, declarative Infrastructure as Code.&lt;br&gt;&lt;br&gt;
SAM lets you describe that same infrastructure: Lambda, API Gateway, IAM permissions in a single YAML file. One command builds it. One command deploys it. Anyone on your team can clone your repo and have the exact same API running in minutes.&lt;/p&gt;

&lt;p&gt;Using the AWS Console is like picking your pizza toppings by hand—great for learning, but slow and easy to mess up. AWS SAM is your digital recipe card. You write the recipe once, hand it to AWS, and it bakes the exact same, perfect pizza with one click.&lt;br&gt;
If a teammate wants that same pizza, you just hand them the recipe.&lt;/p&gt;


&lt;h3&gt;
  
  
  What Is a REST API?
&lt;/h3&gt;

&lt;p&gt;A REST API (Representational State Transfer) is a way for applications to talk to each other over the internet using standard HTTP methods: GET, POST, PUT, DELETE.&lt;br&gt;&lt;br&gt;
Think of it like a waiter in a restaurant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You (the client) place an order (HTTP request).&lt;/li&gt;
&lt;li&gt;  The waiter (the API) takes it to the kitchen (your server/Lambda).&lt;/li&gt;
&lt;li&gt;  The kitchen prepares it and the waiter brings back your food (JSON response).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time you use a weather app, log into a website, or pay online, a REST API is working behind the scenes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why REST Over Other API Styles?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are several ways to design an API but REST remains the industry standard for a wide range of web and mobile applications. Here is how it compares to other common styles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;REST&lt;/strong&gt; (Best for Web &amp;amp; mobile apps): Simple, stateless, works everywhere, maximum configuration control.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;HTTP API&lt;/strong&gt; (Best for High-speed, low-cost use cases): Fewer features but faster performance, less control. &lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;GraphQL&lt;/strong&gt; (Best for Complex, flexible data queries): Overkill for beginners.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WebSocket&lt;/strong&gt; (Best for Real-time chat, live feeds): Wrong tool for request/response pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;AWS offers both REST API and HTTP API in API Gateway. HTTP API is cheaper and faster, but REST API gives you more control like request validation, usage plans, API keys, and detailed metrics. For learning, REST API teaches you more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why REST Is the Industry Standard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Human-readable: URLs like &lt;code&gt;/hello?name=Gloria&lt;/code&gt; make sense instantly&lt;/li&gt;
&lt;li&gt;  Stateless: every request carries everything the server needs, no sessions&lt;/li&gt;
&lt;li&gt;  Works everywhere: browsers, mobile apps, CLI tools, other servers&lt;/li&gt;
&lt;li&gt;  Scales effortlessly: especially paired with serverless like AWS Lambda&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this tutorial, we will build and deploy a REST API to AWS using AWS SAM, complete with unit tests, integration test, a live documentation endpoint and finally deploying to production AWS.&lt;/p&gt;

&lt;p&gt;Let’s build it.&lt;/p&gt;


&lt;h2&gt;
  
  
  What We’re Building
&lt;/h2&gt;

&lt;p&gt;A REST API with three endpoints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;GET /&lt;/code&gt; : Serves a live documentation page&lt;/li&gt;
&lt;li&gt; &lt;code&gt;GET /hello&lt;/code&gt; : Returns a personalised JSON greeting&lt;/li&gt;
&lt;li&gt; &lt;code&gt;GET /get-documentation&lt;/code&gt; : Also serves the documentation page&lt;/li&gt;
&lt;/ol&gt;


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

&lt;p&gt;Before starting, ensure you have these tools installed and configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AWS account&lt;/strong&gt; — New AWS customers can get started at no cost with the AWS Free Tier&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AWS CLI&lt;/strong&gt; — AWS Command Line Interface (configured with your credentials)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Docker Desktop&lt;/strong&gt; — User-friendly platform for container management&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Git Bash&lt;/strong&gt; — Provides a Unix-like terminal for Windows users&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;VS Code&lt;/strong&gt; — Open source AI code editor (or any code editor)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;SAM CLI&lt;/strong&gt; — installed and ready&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Git Bash / curl&lt;/strong&gt; — Command line testing&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pytest&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Quick Verification
&lt;/h2&gt;

&lt;p&gt;Run these commands to confirm everything is set up correctly:&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="c"&gt;# Check AWS CLI  &lt;/span&gt;
aws &lt;span class="nt"&gt;--version&lt;/span&gt;  

&lt;span class="c"&gt;# Verify AWS credentials are configured  &lt;/span&gt;
aws sts get-caller-identity  

&lt;span class="c"&gt;# Check SAM is installed  &lt;/span&gt;
sam &lt;span class="nt"&gt;--version&lt;/span&gt;  

&lt;span class="c"&gt;# Check Python is installed  &lt;/span&gt;
python &lt;span class="nt"&gt;--version&lt;/span&gt;  

&lt;span class="c"&gt;# Check Docker  &lt;/span&gt;
docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Create the Project Structure
&lt;/h2&gt;

&lt;p&gt;Open Git Bash or the Windows command prompt and initialize your project directory by running the following commands:&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="c"&gt;# Check and note your working directory before creating folders  &lt;/span&gt;
&lt;span class="nb"&gt;pwd&lt;/span&gt;  

&lt;span class="c"&gt;# Create project folders  &lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;hello-world-api  

&lt;span class="c"&gt;# Create template.yaml file  &lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;template.yaml  

&lt;span class="c"&gt;# Navigate to hello-world-api and create another folder  &lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;hello-world-api  
&lt;span class="nb"&gt;mkdir &lt;/span&gt;hello_world  

&lt;span class="c"&gt;# Create app.py file inside hello_world  &lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Windows Explorer to navigate to the &lt;code&gt;hello-world-api&lt;/code&gt; folder, right-click on the folder, and select &lt;strong&gt;Open with Code&lt;/strong&gt;. Your folder structure should look like this in VS Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hello-world-api/  
├── hello_world/  
│   └── app.py        ← Lambda &lt;span class="k"&gt;function &lt;/span&gt;goes here  
└── template.yaml     ← SAM infrastructure goes here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Create the Lambda Function
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;app.py&lt;/code&gt; and paste in the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;


&lt;span class="n"&gt;COMMON_HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Only GET endpoints supported
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&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;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Method not allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Routing
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&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;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/get-documentation&lt;/span&gt;&lt;span class="sh"&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;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&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;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryStringParameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html lang=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
        &amp;lt;meta name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;viewport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width=device-width, initial-scale=1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
        &amp;lt;title&amp;gt;Hello API Docs&amp;lt;/title&amp;gt;
        &amp;lt;link rel=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;icon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; href=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
        &amp;lt;style&amp;gt;
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body {
                font-family: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Segoe UI&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, sans-serif;
                background: #0f1117;
                color: #ffffff;
                min-height: 100vh;
                padding: 48px 24px;
            }
            .container { max-width: 720px; margin: 0 auto; }
            .badge {
                display: inline-block;
                background: #ff9900;
                color: #000;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 1px;
                padding: 4px 12px;
                border-radius: 20px;
                margin-bottom: 24px;
                text-transform: uppercase;
            }
            h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
            .subtitle { color: #8b8fa8; font-size: 15px; margin-bottom: 40px; }
            .card {
                background: #1a1d27;
                border: 1px solid #2a2d3e;
                border-radius: 12px;
                padding: 24px;
                margin-bottom: 16px;
            }
            .endpoint-row {
                display: flex;
                align-items: center;
                gap: 12px;
                margin-bottom: 12px;
            }
            .method {
                background: #1a4731;
                color: #4ade80;
                font-size: 12px;
                font-weight: 700;
                padding: 4px 10px;
                border-radius: 6px;
                font-family: monospace;
            }
            .path {
                font-family: monospace;
                font-size: 15px;
                color: #ff9900;
            }
            .description { color: #8b8fa8; font-size: 14px; margin-bottom: 16px; }
            .param-label {
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: 1px;
                color: #555870;
                margin-bottom: 8px;
            }
            .param-row {
                display: flex;
                gap: 12px;
                font-size: 13px;
                padding: 8px 0;
                border-bottom: 1px solid #2a2d3e;
                align-items: baseline;
            }
            .param-row:last-child { border-bottom: none; }
            .param-name { font-family: monospace; color: #ff9900; min-width: 80px; }
            .param-type { color: #555870; min-width: 60px; }
            .param-desc { color: #8b8fa8; }
            .response-box {
                background: #0f1117;
                border: 1px solid #2a2d3e;
                border-radius: 8px;
                padding: 16px;
                font-family: monospace;
                font-size: 13px;
                color: #4ade80;
                margin-top: 16px;
                white-space: pre;
            }
        &amp;lt;/style&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;container&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
            &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;badge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;API Documentation&amp;lt;/div&amp;gt;
            &amp;lt;h1&amp;gt;Hello API&amp;lt;/h1&amp;gt;
            &amp;lt;p class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;A beginner-friendly serverless REST API built with AWS SAM &amp;amp;amp; Lambda &amp;amp;middot; v1.0&amp;lt;/p&amp;gt;

            &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endpoint-row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;GET&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;/hello&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;p class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    Returns a personalised hello message. Pass a name as a query parameter or default to &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
                &amp;lt;/p&amp;gt;

                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Query parameters&amp;lt;/div&amp;gt;
                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;name&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;string&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Optional. The name to greet. Defaults to &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endpoint-row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;GET&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;/get-documentation&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;p class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    Returns this documentation page as an HTML response. Demonstrating that Lambda can serve HTML, not just JSON.
                &amp;lt;/p&amp;gt;

                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;No parameters&amp;lt;/div&amp;gt;
                &amp;lt;div class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;-&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;lt;/span&amp;gt;
                    &amp;lt;span class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;param-desc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;No query parameters required.&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html_content&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_message&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;h3&gt;
  
  
  2.1  Understanding the Lambda Function Code
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;app.py&lt;/code&gt; contains four functions. Here's what each one does:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1.1 &lt;code&gt;def lambda_handler(event, context)&lt;/code&gt;: the entry point&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every Lambda function must have this exact signature. AWS calls this function and passes two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;event&lt;/code&gt; — information about what triggered the Lambda; in our case, the HTTP request&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;context&lt;/code&gt; — metadata about the function execution (request ID, time remaining, etc.)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These two lines extract the HTTP method and path from the incoming request and store them in variables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;event&lt;/code&gt; only exists when a request comes in. It is created fresh for every single invocation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The routing block then directs each request to the right handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Only GET endpoints supported  
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&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;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Method not allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  

    &lt;span class="c1"&gt;# Routing  
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&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;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/get-documentation&lt;/span&gt;&lt;span class="sh"&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;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&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;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good to know:&lt;/strong&gt; The &lt;code&gt;error_response(405, ...)&lt;/code&gt; for non-GET methods is not just good practice. It also saves Lambda cost by rejecting invalid requests immediately at the API Gateway before they reach Lambda.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2.1.2 &lt;code&gt;def hello_handler(event)&lt;/code&gt;: the main API logic&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The hello_handler function serves as the central API logic. It processes &lt;code&gt;GET /hello&lt;/code&gt; requests by first extracting the &lt;code&gt;name&lt;/code&gt; query parameter from each request, builds the JSON response body and return data(hello greeting) to the client. This is where we implement the fundamental pattern of every API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extract parameters from the event (request):&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;query_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryStringParameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First, it extracts the query parameters from the event. For example, if you type &lt;code&gt;http://localhost:3000/hello?name=John&lt;/code&gt; in your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;query_params&lt;/code&gt; would be: &lt;code&gt;{"name": "John"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;{}&lt;/code&gt; is a default : it tells Lambda that if there are no query parameters, use an empty dictionary instead of crashing.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;or {}&lt;/code&gt; is important because API Gateway can pass &lt;code&gt;None&lt;/code&gt; for queryStringParameters when there are no query params, and calling .get() on &lt;code&gt;None&lt;/code&gt; would crash.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build the response body:&lt;/strong&gt;
The response must follow this exact format for API Gateway:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Return data:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;COMMON_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_body&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;Lambda must return this exact format for API Gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;statusCode: 200&lt;/code&gt; : HTTP success code (200 = OK)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;headers&lt;/code&gt; : HTTP headers to send back, including:&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Content-Type: application/json&lt;/code&gt; : tells the browser "this is JSON"&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; : allows any website to call this API (CORS)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;body&lt;/code&gt; : the actual response (must be a STRING, so we convert the dictionary to JSON with &lt;code&gt;json.dumps()&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It is important to use &lt;code&gt;json.dumps()&lt;/code&gt; otherwise Lambda will return a raw dictionary as the body, which Lambda will accept, but API Gateway won't know what to do with it and will fail to properly format the response for the client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;2.1.3 &lt;code&gt;def documentation_handler()&lt;/code&gt; — the HTML endpoint&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
This is the part that surprises most beginners: &lt;strong&gt;Lambda can return HTML, not just JSON&lt;/strong&gt;. The only thing that changes is one header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# ← this tells the browser to render a page
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare above content-Type to /hello:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# ← this tells the browser to display raw JSON
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same Lambda, same API Gateway but completely different browser behavior, controlled by a single header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.1.4 &lt;code&gt;def error_response(status_code, message)&lt;/code&gt; — consistent error handling&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every error response goes through this function, which ensures CORS headers are always included. This matters because if a browser receives a &lt;code&gt;404&lt;/code&gt; without a CORS header, it blocks the response entirely and the client gets a confusing network error instead of the actual error message.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To this point you have learned the four fundamentals of every API: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;receive a request, &lt;/li&gt;
&lt;li&gt;extract parameters&lt;/li&gt;
&lt;li&gt;build a response, and &lt;/li&gt;
&lt;li&gt;return data &lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Master this pattern and you master every API you'll ever build. Well done for coming this far!&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Define Your Infrastructure in &lt;code&gt;template.yaml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;template.yaml&lt;/code&gt; and paste in the following:&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="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless-2016-10-31&lt;/span&gt;

&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hello API - A beginner-friendly serverless REST API built with AWS SAM and Lambda&lt;/span&gt;

&lt;span class="na"&gt;Globals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&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;python3.11&lt;/span&gt;
    &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;128&lt;/span&gt;

&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# API Gateway&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Api&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;HelloWorldApi&lt;/span&gt;
      &lt;span class="na"&gt;StageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prod&lt;/span&gt;
      &lt;span class="na"&gt;TracingEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;MethodSettings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ResourcePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/*'&lt;/span&gt;
          &lt;span class="na"&gt;HttpMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingBurstLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingRateLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
          &lt;span class="na"&gt;MetricsEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;Cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AllowMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'GET,OPTIONS'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowOrigin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'*'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'Content-Type'"&lt;/span&gt;

  &lt;span class="c1"&gt;# Lambda Function&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunction&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello_world/&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;app.lambda_handler&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Beginner-friendly serverless Hello API&lt;/span&gt;

      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

        &lt;span class="na"&gt;RootApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/&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;HelloApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/hello&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;DocumentationApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/get-documentation&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;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;RootUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Root URL - serves documentation page&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"&lt;/span&gt;

  &lt;span class="na"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hello endpoint&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"&lt;/span&gt;

  &lt;span class="na"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Documentation endpoint&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/get-documentation"&lt;/span&gt;

  &lt;span class="na"&gt;HelloWorldFunctionArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lambda function ARN&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunction.Arn&lt;/span&gt;

  &lt;span class="na"&gt;HelloWorldFunctionIamRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IAM role ARN created for the Lambda function&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunctionRole.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the SAM Template
&lt;/h3&gt;

&lt;p&gt;The template is organized into four sections.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Version declaration and Transform&lt;/strong&gt;&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="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells AWS “I’m using CloudFormation syntax from 2010–09–09.”&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="na"&gt;Transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless-2016-10-31&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line converts the SAM syntax into full CloudFormation. SAM is like a shortcut language that gets translated into longer CloudFormation code. Without this, AWS wouldn’t understand SAM-specific resources.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;Globals&lt;/code&gt;&lt;/strong&gt;&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="na"&gt;Globals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;Function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10&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;python3.11&lt;/span&gt;  
    &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;128&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are default settings applied to every Lambda function in the template. Setting them once here means you don’t have to repeat them for every function.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Timeout: 10&lt;/code&gt; : kills the function if it runs longer than 10 seconds, preventing runaway costs&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Runtime: python3.11&lt;/code&gt;: the Python version used to run the code&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;MemorySize: 128&lt;/code&gt;: the minimum RAM allocation, keeping costs low for a simple API&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;Resources&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This section contains everything that AWS creates. The template groups resources into two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;API Gateway&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The template defines the API Gateway resource explicitly so we can attach settings like throttling and CORS:&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="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# API Gateway&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Api&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;HelloWorldApi&lt;/span&gt;
      &lt;span class="na"&gt;StageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prod&lt;/span&gt;
      &lt;span class="na"&gt;TracingEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;MethodSettings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ResourcePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/*'&lt;/span&gt;
          &lt;span class="na"&gt;HttpMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingBurstLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingRateLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
          &lt;span class="na"&gt;MetricsEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;Cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AllowMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'GET,OPTIONS'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowOrigin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'*'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'Content-Type'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;StageName: Prod&lt;/code&gt;: creates &lt;code&gt;/Prod&lt;/code&gt; in your URL: &lt;code&gt;https://abc123.execute-api.us-east-1.amazonaws.com/Prod/hello&lt;/code&gt;. You can have multiple stages later: &lt;code&gt;Dev&lt;/code&gt;, &lt;code&gt;Test&lt;/code&gt;, &lt;code&gt;Prod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;TracingEnabled: true&lt;/code&gt; : enables AWS X-Ray for distributed tracing, useful for debugging performance issues.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;ThrottlingBurstLimit: 100&lt;/code&gt; / &lt;code&gt;ThrottlingRateLimit: 50&lt;/code&gt;: limits how many requests the API can handle per second, protecting both your Lambda function and your AWS bill.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;MetricsEnabled: true&lt;/code&gt;: sends API metrics (request count, errors, latency) to CloudWatch.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Cors&lt;/code&gt;: allows browsers to call this API from any website. &lt;code&gt;AllowOrigin: "*"&lt;/code&gt; is fine for learning; in production you would restrict this to specific domains.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;Lambda Function&lt;/code&gt;&lt;/strong&gt;&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;# Lambda Function&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunction&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello_world/&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;app.lambda_handler&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Beginner-friendly serverless Hello API&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;FunctionName: HelloWorldFunction&lt;/code&gt;: the name AWS gives this Lambda function.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;CodeUri: hello_world/&lt;/code&gt; : tells SAM where your Lambda code lives.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Handler: app.lambda_handler&lt;/code&gt; : the format is &lt;code&gt;filename.function_name&lt;/code&gt;, so this means "call the &lt;code&gt;lambda_handler&lt;/code&gt; function inside &lt;code&gt;app.py&lt;/code&gt;", i.e. &lt;code&gt;def lambda_handler(event, context)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Description&lt;/code&gt; : a short description of what the function does.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;code&gt;Events&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Events&lt;/code&gt; block describes which API routes trigger the Lambda function. Three events are defined in this template:&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="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

        &lt;span class="na"&gt;RootApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/&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;HelloApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/hello&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;DocumentationApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/get-documentation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; &lt;code&gt;**RootApi**&lt;/code&gt; : handles &lt;code&gt;GET /&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**HelloApi**&lt;/code&gt;: handles &lt;code&gt;GET /hello&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**DocumentationApi**&lt;/code&gt;: handles &lt;code&gt;GET /get-documentation&lt;/code&gt; request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;RestApiId: !Ref HelloWorldApi&lt;/code&gt; : attaches the route to the API Gateway defined above. Without this, SAM would automatically create a second, separate API Gateway.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Path&lt;/code&gt;: defines the API endpoint URL (e.g. &lt;code&gt;/hello&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Method: get&lt;/code&gt;: only &lt;code&gt;GET&lt;/code&gt; requests are allowed on these routes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full Resources block:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# API Gateway&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Api&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;HelloWorldApi&lt;/span&gt;
      &lt;span class="na"&gt;StageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prod&lt;/span&gt;
      &lt;span class="na"&gt;TracingEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;MethodSettings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ResourcePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/*'&lt;/span&gt;
          &lt;span class="na"&gt;HttpMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingBurstLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
          &lt;span class="na"&gt;ThrottlingRateLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
          &lt;span class="na"&gt;MetricsEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="na"&gt;Cors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AllowMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'GET,OPTIONS'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowOrigin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'*'"&lt;/span&gt;
        &lt;span class="na"&gt;AllowHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'Content-Type'"&lt;/span&gt;

  &lt;span class="c1"&gt;# Lambda Function&lt;/span&gt;
  &lt;span class="na"&gt;HelloWorldFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunction&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello_world/&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;app.lambda_handler&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Beginner-friendly serverless Hello API&lt;/span&gt;

      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

        &lt;span class="na"&gt;RootApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/&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;HelloApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/hello&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;DocumentationApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HelloWorldApi&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;/get-documentation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;&lt;code&gt;Outputs&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are values displayed on the screen after deployment. They give you the live URLs and resource identifiers for your deployed stack.&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="na"&gt;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;RootUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Root URL - serves documentation page&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"&lt;/span&gt;

  &lt;span class="na"&gt;HelloApiUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hello endpoint&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"&lt;/span&gt;

  &lt;span class="na"&gt;DocumentationUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Documentation endpoint&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/get-documentation"&lt;/span&gt;

  &lt;span class="na"&gt;HelloWorldFunctionArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lambda function ARN&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunction.Arn&lt;/span&gt;

  &lt;span class="na"&gt;HelloWorldFunctionIamRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IAM role ARN created for the Lambda function&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;HelloWorldFunctionRole.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding output
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;**RootUrl**&lt;/code&gt; : the root endpoint of your API (&lt;code&gt;/&lt;/code&gt;). This is where the documentation page is served.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;**HelloApiUrl**&lt;/code&gt; : the &lt;code&gt;/hello&lt;/code&gt; endpoint URL.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;**DocumentationUrl**&lt;/code&gt;: the &lt;code&gt;/get-documentation&lt;/code&gt; endpoint URL.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;**HelloWorldFunctionArn**&lt;/code&gt; : the Amazon Resource Name (ARN) of your Lambda function. Useful for referencing the function in other AWS services or IAM policies.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;**HelloWorldFunctionIamRole**&lt;/code&gt;: the ARN of the IAM role automatically created for your Lambda function by SAM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Understanding &lt;code&gt;!Sub&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;!Sub&lt;/code&gt; is a CloudFormation intrinsic function that means &lt;strong&gt;"substitute variables"&lt;/strong&gt;. It replaces placeholders in the string with their actual values at deploy time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;${HelloWorldApi}&lt;/code&gt; — the API Gateway ID, auto-generated by AWS when the stack is created.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;${AWS::Region}&lt;/code&gt; — the AWS region the stack is deployed to (e.g. &lt;code&gt;us-east-1&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a value like:&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="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes something like:&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="s"&gt;https://abc123.execute-api.us-east-1.amazonaws.com/Prod/hello&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding &lt;code&gt;!GetAtt&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;!GetAtt&lt;/code&gt; retrieves an &lt;strong&gt;attribute&lt;/strong&gt; of a resource created in the same template. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;!GetAtt HelloWorldFunction.Arn&lt;/code&gt;:gets the ARN of the &lt;code&gt;HelloWorldFunction&lt;/code&gt; Lambda.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;!GetAtt HelloWorldFunctionRole.Arn&lt;/code&gt; : gets the ARN of the IAM role SAM automatically created for your Lambda.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Summary of what the SAM Template Does
&lt;/h3&gt;

&lt;p&gt;The SAM template performs 5 major functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;**Globals**&lt;/code&gt; — sets default Lambda settings (timeout, runtime, memory).&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**API Gateway**&lt;/code&gt; — creates and configures the API Gateway with throttling, CORS, and metrics.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**Function**&lt;/code&gt; — creates the Lambda function and wires up the code.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**Events**&lt;/code&gt; — connects API routes (&lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/hello&lt;/code&gt;, &lt;code&gt;/get-documentation&lt;/code&gt;) to the Lambda function.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**Outputs**&lt;/code&gt; — displays the live URLs and resource ARNs after deployment.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 4: A Note on Security
&lt;/h2&gt;

&lt;p&gt;For a beginner-friendly REST API, there isn’t much security configuration needed. However, a few things in this template already protect you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Method validation in&lt;/strong&gt; &lt;code&gt;**app.py**&lt;/code&gt; : the &lt;code&gt;if method != "GET"&lt;/code&gt; check rejects all non-GET requests before any real work happens, protecting your Lambda function from unnecessary invocations and cost.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Throttling&lt;/strong&gt; : &lt;code&gt;ThrottlingBurstLimit&lt;/code&gt; and &lt;code&gt;ThrottlingRateLimit&lt;/code&gt; protect against traffic spikes and accidental (or intentional) overuse.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;IAM role&lt;/strong&gt; : SAM automatically creates a least-privilege IAM role for your Lambda function. You don’t define it manually — SAM handles it, and the role ARN appears in your Outputs after deployment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 5: Validate the Template
&lt;/h3&gt;

&lt;p&gt;Before building, check that your &lt;code&gt;template.yaml&lt;/code&gt; is valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see: &lt;code&gt;template.yaml is a valid SAM Template&lt;/code&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%2Fsc2gqypeczx1sr9kx0a3.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%2Fsc2gqypeczx1sr9kx0a3.png" alt="Figure 1: sam validate command and output" width="800" height="65"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Build the Application
&lt;/h2&gt;

&lt;p&gt;This packages your Lambda function and prepares everything for deployment or local testing. Run this every time you change &lt;code&gt;app.py&lt;/code&gt; or &lt;code&gt;template.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam build
&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%2Fdgjnz7dxojktj3b57y11.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%2Fdgjnz7dxojktj3b57y11.png" alt="Figure 2: The requirements.txt warning is expected — sam build succeeded because this API uses only Python’s built-in libraries." width="799" height="266"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Run the API Locally
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;/strong&gt; Docker Desktop must be running before you execute &lt;code&gt;sam local start-api&lt;/code&gt;. SAM uses Docker to simulate the Lambda environment locally. If Docker is not started, you will see an error like &lt;code&gt;Error: Running AWS SAM projects locally requires Docker&lt;/code&gt; and the local API will not start.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts a local API server on the default port 3000. If port 3000 is already in use (for example, by a React or Node.js dev server), use a different port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api &lt;span class="nt"&gt;--port&lt;/span&gt; 8080
&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%2Ffq700jktvzv8reaas0ug.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%2Ffq700jktvzv8reaas0ug.png" alt="Figure 3: command and output of  raw `sam local start-api` endraw " width="798" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The first time you run &lt;code&gt;sam local start-api&lt;/code&gt;, don't panic when you see a stream of dots filling your terminal. That's completely normal! SAM is pulling the Python 3.11 Lambda runtime image from Docker Hub layer by layer. Depending on your internet speed, this can take a minute or two or more. Once it's done, you will see &lt;code&gt;Mounting HelloWorldFunction at http://127.0.0.1:3000&lt;/code&gt; and your API is ready. On future runs, use &lt;code&gt;--skip-pull-image&lt;/code&gt; to skip this step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8: Test the Endpoints
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8.1 Using the Browser
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt; &lt;code&gt;**GET /hello**&lt;/code&gt; &lt;strong&gt;endpoint without name parameter should default to world&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once your terminal shows &lt;code&gt;Running on http://127.0.0.1:3000&lt;/code&gt; follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Keep the terminal running. Do not close it. Closing it stops your local API.&lt;/li&gt;
&lt;li&gt; Open Chrome browser and enter &lt;code&gt;http://localhost:3000/hello&lt;/code&gt; in the URL bar&lt;/li&gt;
&lt;li&gt; Switch back to your terminal and you will should see a log line confirming the request was handled, with an HTTP &lt;code&gt;200&lt;/code&gt; status code&lt;/li&gt;
&lt;li&gt; Switch back to your browser to see your JSON response&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%2Fbe5sfpwpqd0ss3zne8nn.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%2Fbe5sfpwpqd0ss3zne8nn.png" alt="Figure 4:browser showing response of  raw `http://localhost:3000/hello` endraw " width="800" height="101"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt; &lt;code&gt;**GET /hello**&lt;/code&gt; &lt;strong&gt;endpoint with name parameter should show greeting with your name.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Open chrome browser and type this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://localhost:3000/hello?name=YourName&lt;/code&gt; — the message updates with your name&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%2Fnsfleq7ik8zlp50r1vqj.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%2Fnsfleq7ik8zlp50r1vqj.png" alt="Figure 5: browser showing response on  raw `http://localhost:3000/hello?name=Gloria` endraw " width="800" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;GET /get-documentation&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;— should serve the documentation page:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter this in a new tab in your browser:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://localhost:3000/get-documentation&lt;/code&gt; — you should see the styled documentation page&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%2Ffzw4uy6m0392jr1udors.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%2Ffzw4uy6m0392jr1udors.png" alt="Figure 6: browser displaying  raw `/get-documentation` endraw  endpoint" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the root&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;/&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;endpoint — should also serve the documentation page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enter this in a new tab in your browser: &lt;code&gt;http://localhost:3000/&lt;/code&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%2Feda85ua57a1l2047kx9m.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%2Feda85ua57a1l2047kx9m.png" alt="Figure 7: root  raw `/` endraw  endpoint also display API documentation" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8.2 Using curl (Git Bash or Command Prompt)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sam local start-api&lt;/code&gt; takes over the terminal it runs in. It stays running and logs every request in real time. To test with curl, you need a second terminal alongside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows users:&lt;/strong&gt; If you are using PowerShell, &lt;code&gt;curl&lt;/code&gt; is actually an alias for &lt;code&gt;Invoke-WebRequest&lt;/code&gt; and gives a very different output — verbose headers, status codes, and a security warning. Use &lt;strong&gt;Git Bash&lt;/strong&gt; for all the commands in this section instead. Alternatively, type &lt;code&gt;curl.exe&lt;/code&gt; instead of &lt;code&gt;curl&lt;/code&gt; in PowerShell to call the real curl binary directly and get the same clean output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s the flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terminal 1 — start the API and leave it running:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Terminal 2 — open a new Git Bash window and run this curl command to test the &lt;code&gt;/hello&lt;/code&gt; endpoint:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terminal 2 shows the actual JSON response from curl.&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%2F6hs48myzb1oh8adq2keu.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%2F6hs48myzb1oh8adq2keu.png" alt="Figure 8:  raw `curl http://localhost:3000/hello` endraw  and expected output" width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch back to Terminal 1. You will see the request logged with the HTTP status code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Invoking app.lambda_handler &lt;span class="o"&gt;(&lt;/span&gt;python3.11&lt;span class="o"&gt;)&lt;/span&gt;                                                                                                    
Requested to skip pulling images ...                                                                                                        

Mounting C:&lt;span class="se"&gt;\U&lt;/span&gt;sers&lt;span class="se"&gt;\g&lt;/span&gt;loba&lt;span class="se"&gt;\h&lt;/span&gt;ello-world-api&lt;span class="se"&gt;\.&lt;/span&gt;aws-sam&lt;span class="se"&gt;\b&lt;/span&gt;uild&lt;span class="se"&gt;\H&lt;/span&gt;elloWorldFunction as /var/task:ro,delegated, inside runtime container               
START RequestId: c607d6bd-84bd-4eab-8588-e971dc303056 Version: &lt;span class="nv"&gt;$LATEST&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;DEBUG] Lambda invoked - method: GET, path: /hello
END RequestId: e4960c8a-e470-4b02-872c-852722317cbd
REPORT RequestId: e4960c8a-e470-4b02-872c-852722317cbd  Init Duration: 0.20 ms  Duration: 144.05 ms     Billed Duration: 145 ms Memory Size: 128 MB Max Memory Used: 128 MB

2026-06-29 14:56:08 127.0.0.1 - - &lt;span class="o"&gt;[&lt;/span&gt;29/Jun/2026 14:56:08] &lt;span class="s2"&gt;"GET /hello HTTP/1.1"&lt;/span&gt; 200 -
&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%2Fedpy103tmcb8pk8c9zjr.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%2Fedpy103tmcb8pk8c9zjr.png" alt="Figure 9: http request logs in git bash" width="798" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This two-terminal pattern is standard in API development. One terminal runs the server, the other sends requests. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;/hello&lt;/code&gt; endpoint with a name parameter:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Type this in terminal 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/hello?name&lt;span class="o"&gt;=&lt;/span&gt;Gloria
&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%2Fyo9jnzxeum9j23gkwf3i.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%2Fyo9jnzxeum9j23gkwf3i.png" alt="Figure 10:  raw `curl http://localhost:3000/hello?name=Gloria` endraw  and expected output" width="800" height="43"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test &lt;code&gt;GET /get-documentation&lt;/code&gt; — should serve the documentation page:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/get-documentation
&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%2F75zoc90e9rpqb7hkppck.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%2F75zoc90e9rpqb7hkppck.png" alt="Figure 11: truncated output of the command  raw `curl http://localhost:3000/get-documentation` endraw " width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the root &lt;code&gt;/&lt;/code&gt; endpoint should display the documentation page source:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/
&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%2Fwo6xdoaswmdlighvvqj7.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%2Fwo6xdoaswmdlighvvqj7.png" alt="Figure 12: truncated output of  raw `curl http://localhost:3000/` endraw " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8.3 Testing Non-GET Methods
&lt;/h3&gt;

&lt;p&gt;Once your GET requests are working, it’s worth sending one or two non-GET requests to see exactly how API Gateway protects your Lambda function.&lt;br&gt;&lt;br&gt;
Open a &lt;strong&gt;second terminal&lt;/strong&gt; (leave Terminal 1 running with &lt;code&gt;sam local start-api&lt;/code&gt;) and run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Browsers can only send &lt;code&gt;GET&lt;/code&gt; requests from the address bar. You cannot test &lt;code&gt;POST&lt;/code&gt; or &lt;code&gt;PUT&lt;/code&gt; methods directly in a browser. Use curl, Git Bash, or curl.exe (PowerShell) in a second terminal as shown below.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test POST — should be blocked  &lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/hello  

&lt;span class="c"&gt;# Test PUT - should be blocked  &lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost:3000/hello
&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%2Ffsa3korycqcbli9vb9p3.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%2Ffsa3korycqcbli9vb9p3.png" alt="Figure 13:  raw `POST` endraw  and  raw `PUT` endraw  request logs in Git Bash" width="799" height="177"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Now switch back to &lt;strong&gt;Terminal 1&lt;/strong&gt; and compare the POST and PUT request logs(Figure 14) with GET request logs in Figure 15:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;POST and PUT request logs&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%2Fn98gk43kmk6x5ojylxxv.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%2Fn98gk43kmk6x5ojylxxv.png" alt="Figure 14:  raw `POST` endraw  and  raw `PUT` endraw  request logs in git bash" width="796" height="58"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GET request log&lt;/strong&gt;&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%2Fcdxuigx338fup4c3j0ge.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%2Fcdxuigx338fup4c3j0ge.png" alt="Figure 15:  raw `GET` endraw  request logs in git bash" width="798" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notice what's missing in POST and PUT request logs(Figure 14):&lt;/strong&gt; &lt;br&gt;
No &lt;code&gt;START RequestId&lt;/code&gt;, no &lt;code&gt;Billed Duration&lt;/code&gt;. The &lt;code&gt;POST&lt;/code&gt; and &lt;code&gt;PUT&lt;/code&gt; requests were rejected by API Gateway — they never reached your Lambda function. This is exactly what method validation is for. It protects both your function and your AWS bill.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why does API Gateway return&lt;/strong&gt; &lt;code&gt;**{"message": "Missing Authentication Token"}**&lt;/code&gt; &lt;strong&gt;instead of your Lambda's 405 error?&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;When you send a &lt;code&gt;POST&lt;/code&gt; or &lt;code&gt;PUT&lt;/code&gt; request, API Gateway checks your &lt;code&gt;template.yaml&lt;/code&gt; first. Since only &lt;code&gt;GET&lt;/code&gt; is configured for these routes, API Gateway doesn't recognize the method and returns its own default error, before your Lambda function is ever invoked. Despite the confusing name, this message simply means "I don't know what to do with this request." Your Lambda's &lt;code&gt;405&lt;/code&gt; error would only appear if Lambda was invoked directly, bypassing API Gateway entirely. This is actually good news: the block happens at the gateway level, so Lambda is never called and you are never billed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 9: Automated Testing — Unit Tests
&lt;/h2&gt;

&lt;p&gt;A unit test is a Python script that calls your Lambda function &lt;strong&gt;directly&lt;/strong&gt; — without API Gateway, without SAM, without a running server and checks that the output is exactly what you expected.&lt;br&gt;&lt;br&gt;
Instead of manually opening a browser and typing &lt;code&gt;localhost:3000/hello?name=Gloria&lt;/code&gt; every time you make a change, a unit test does that check automatically in one command.&lt;br&gt;&lt;br&gt;
A unit test checks one small piece of your code in complete isolation. No internet. No AWS. No real API Gateway. Just your Python functions.&lt;br&gt;&lt;br&gt;
Unit tests catch bugs before your code reaches AWS. Instead of sending a real HTTP request, you simulate the inputs (the &lt;code&gt;event&lt;/code&gt; dictionary that API Gateway normally builds) and check the outputs. This is fast, free, and repeatable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Automated Testing Catches More Than Console Testing
&lt;/h3&gt;

&lt;p&gt;When you test on the console or browser, you only see what the surface shows. For example, API Gateway does not explicitly display &lt;code&gt;"Endpoint not found"&lt;/code&gt; when you hit an unknown path — it just returns a generic error and you might think everything is fine.&lt;br&gt;&lt;br&gt;
Automated tests look beneath the surface. They check the actual values in the response — the status code, the error message, the headers: things the console never shows you. That's why you will notice we have far more tests here than things you could verify manually in a browser. A browser tells you "it worked" or "it didn't." A unit test tells you exactly what worked, what didn't, and why.&lt;br&gt;&lt;br&gt;
This is the real power of automated testing: it verifies the &lt;strong&gt;contract&lt;/strong&gt; of your code, not just the appearance.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 10: Set Up the Test Structure
&lt;/h2&gt;
&lt;h3&gt;
  
  
  10.1 Create the &lt;code&gt;__init__.py&lt;/code&gt; Files
&lt;/h3&gt;

&lt;p&gt;These are empty files that tell Python “treat this folder as a package.” Without them, your imports will fail.&lt;br&gt;&lt;br&gt;
Run these commands from your project root:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Git Bash:&lt;/strong&gt;&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="c"&gt;# Create empty __init__.py files&lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;hello_world/__init__.py
&lt;span class="nb"&gt;touch &lt;/span&gt;tests/__init__.py
&lt;span class="nb"&gt;touch &lt;/span&gt;tests/unit/__init__.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On Windows (Command Prompt):&lt;/strong&gt;&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;type &lt;/span&gt;nul &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hello_world&lt;span class="se"&gt;\_&lt;/span&gt;_init__.py
&lt;span class="nb"&gt;type &lt;/span&gt;nul &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tests&lt;span class="se"&gt;\_&lt;/span&gt;_init__.py
&lt;span class="nb"&gt;type &lt;/span&gt;nul &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tests&lt;span class="se"&gt;\u&lt;/span&gt;nit&lt;span class="se"&gt;\_&lt;/span&gt;_init__.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10.2 Create a &lt;code&gt;pytest.ini&lt;/code&gt; File
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;pytest.ini&lt;/code&gt; file in the root of your 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="c"&gt;# Create pytest.ini files  &lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;hello-world-api/pytest.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this content:&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="o"&gt;[&lt;/span&gt;pytest]  
pythonpath &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line adds the project root to Python’s search path. Without it, &lt;code&gt;from hello_world.app import ...&lt;/code&gt; fails because Python can't find the &lt;code&gt;hello_world&lt;/code&gt; folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  10.3 Create the &lt;code&gt;tests/unit/test_app.py&lt;/code&gt; File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create test_app.py files  &lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;hello-world-api/tests/unit/test_app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your project should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello-world-api/
├── hello_world/
│   ├── __init__.py       ← makes it a Python package (importable)
│   └── app.py
├── tests/
│   ├── __init__.py       ← makes tests a package
│   └── unit/
│       ├── __init__.py   ← makes unit a package
│       └── test_app.py   ← your test file
├── pytest.ini            ← tells pytest where to find your code
└── template.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;test_app.py&lt;/code&gt; and paste in the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hello_world.app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_response&lt;/span&gt;

&lt;span class="c1"&gt;# In real life, AP1 Gateway builds the "event" dict and passes it to Lambda.
# In unit test, you build the dict yourself. This is called mocking
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Build a minimal API Gateway event dictionary.

    Args:
        method (str): HTTP method, e.g. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
        path   (str): URL path, e.g. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
        query_params (dict): Optional query string params, e.g. {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gloria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}

    Returns:
        dict: A fake API Gateway event our Lambda can process
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queryStringParameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# SECTION 1: TEST ROUTING
# Test that Lambda_handler sends request to the right places
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_hello_returns_200&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;GET /hello should return a greeting: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello-World! (status 200).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_documentation_returns_200&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get /get_documentation should return the documenttion page (status 200).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/get-documentation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_root_returns_200&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get / should return the documentation page (status 200).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_path_returns_404_error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    GET /nonexistent path should return 404 error - Endpoint Not Found.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/nonexistent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_unknown_method_returns_405&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;POST request should return 405 error - Method Not Allowed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_put_method_returns_405&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    PUT requst should return 405 error - Method not Allowed&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;

&lt;span class="c1"&gt;# SECTION 2: TEST HELLO HANDLER
# Test the actual logic of /hello endpoint
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_handler_without_name_parameter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;/hello without name parameter should default to &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello-World!&lt;/span&gt;&lt;span class="sh"&gt;'"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hell0_handler_with_name_parameter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;/hello?name=Gloria should return &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello, Gloria!&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gloria&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Gloria!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_handler_with_empty_queryparams_dict&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_handler_response_contains_timestamp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mock_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hello_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;


&lt;span class="c1"&gt;# SECTION 3: TEST DOCUMENTATION HANDLER
# Test /get-documentation and / endpoints
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_documentation_handler_returns_200&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Test /get-documentation returns status 200&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_documentation_content_type_is_html&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;/get-documentation content-Type is &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;test/html&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;documentation_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="c1"&gt;# SECTION 4
# TEST ERROR
&lt;/span&gt;    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    WHY TEST THE ERROR HELPER?
    error_response is used for both 404 and 405. If it breaks, 
    the entire error handling breaks.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_404_error_response&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Test 404 error response has correct status code and error message
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;error_response&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_405_error_response&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Test 405 error response has correct status code and error message
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Method not allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Method not allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_error_response_cors_header_present&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Error responses needs CORS header.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoint not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code Explanation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mock_event(method="GET", path="/", query_params=None)&lt;/code&gt; is a helper function that builds a fake API Gateway event.&lt;/p&gt;

&lt;p&gt;In production, API Gateway builds the event dictionary and passes it to Lambda. In tests, you build it yourself. This is called &lt;strong&gt;mocking&lt;/strong&gt;. You only include the fields your &lt;code&gt;app.py&lt;/code&gt; actually reads, keeping it minimal and focused.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Routing
&lt;/h3&gt;

&lt;p&gt;These tests hit &lt;code&gt;lambda_handler&lt;/code&gt; — the front door of your API. If routing breaks, everything breaks. They verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;GET /&lt;/code&gt; returns 200&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;GET /hello&lt;/code&gt; returns 200&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;GET /get-documentation&lt;/code&gt; returns 200&lt;/li&gt;
&lt;li&gt;  Unknown paths return &lt;code&gt;404&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  Non-GET methods (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;) return 405&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test Hello Handler
&lt;/h3&gt;

&lt;p&gt;Notice &lt;code&gt;json.loads()&lt;/code&gt;. Lambda returns &lt;code&gt;body&lt;/code&gt; as a JSON string, not a dict. You must parse it back before checking individual fields.&lt;br&gt;&lt;br&gt;
These tests verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Named greeting works (&lt;code&gt;?name=Gloria&lt;/code&gt; → &lt;code&gt;Hello, Gloria!&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;  Default fallback works (no name → &lt;code&gt;Hello, World!&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;  Both &lt;code&gt;None&lt;/code&gt; and &lt;code&gt;{}&lt;/code&gt; query params are handled — API Gateway can send either&lt;/li&gt;
&lt;li&gt;  Response always includes a &lt;code&gt;timestamp&lt;/code&gt; field&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Test Documentation Endpoint
&lt;/h3&gt;

&lt;p&gt;Two things worth noting here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The documentation endpoint returns &lt;code&gt;text/html&lt;/code&gt; — tested explicitly because the wrong &lt;code&gt;Content-Type&lt;/code&gt; would cause browsers to display raw text instead of a rendered page&lt;/li&gt;
&lt;li&gt;  Even error responses need CORS headers. If a browser gets a 404 without a CORS header, the browser blocks it and the client sees a confusing network error instead of the actual 404 message&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Test Error Response
&lt;/h3&gt;

&lt;p&gt;Each function tests one specific aspect of &lt;code&gt;error_response()&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;test_404_error_response&lt;/code&gt;: test 404 error status code and error message are both correct.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;test_405_error_response&lt;/code&gt;: test 405 error status code  and error message are both correct. &lt;/li&gt;
&lt;li&gt;  &lt;code&gt;test_error_response_cors_header_present&lt;/code&gt;: CORS headers must be present even on errors. Without them, browsers block the error response entirely and the client sees a confusing network error instead of the actual 404 or 405 message.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Step 11: Run the Unit Tests
&lt;/h2&gt;

&lt;p&gt;First, install pytest if you haven’t already done so from the prerequisite section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this from your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/unit/test_app.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; flag means verbose. It prints each test name so you can see exactly what passed or failed.&lt;br&gt;&lt;br&gt;
You should see something like this:&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;test&lt;/span&gt;/unit/test_app.py::test_hello_handler_response_contains_timestamp PASSED                                                                                 &lt;span class="o"&gt;[&lt;/span&gt; 66%]
&lt;span class="nb"&gt;test&lt;/span&gt;/unit/test_app.py::test_documentation_handler_returns_200 PASSED                                                                                         &lt;span class="o"&gt;[&lt;/span&gt; 73%]
&lt;span class="nb"&gt;test&lt;/span&gt;/unit/test_app.py::test_documentation_content_type_is_html PASSED                                                                                        &lt;span class="o"&gt;[&lt;/span&gt; 80%]
&lt;span class="nb"&gt;test&lt;/span&gt;/unit/test_app.py::test_404_error_response PASSED                                                                                                        &lt;span class="o"&gt;[&lt;/span&gt; 86%]
&lt;span class="nb"&gt;test&lt;/span&gt;/unit/test_app.py::test_405_error_response PASSED                                                                                                        &lt;span class="o"&gt;[&lt;/span&gt; 93%]
&lt;span class="nb"&gt;test&lt;/span&gt;/unit/test_app.py::test_error_response_cors_header_present PASSED                                                                                        &lt;span class="o"&gt;[&lt;/span&gt;100%]

&lt;span class="o"&gt;=======================================================================&lt;/span&gt; 15 passed &lt;span class="k"&gt;in &lt;/span&gt;0.16s &lt;span class="o"&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%2F2gukrj8l5ull3biy1rfx.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%2F2gukrj8l5ull3biy1rfx.png" alt="Figure 16: pytest output" width="799" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;15 tests. 0.16 seconds. No AWS account needed.&lt;br&gt;
Every time you change &lt;code&gt;app.py&lt;/code&gt;, run pytest before you deploy. If a test fails, you caught the bug locally, not in production.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The pattern is simple: write code → write tests → deploy with confidence.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After the unit tests come integration tests, which are covered in Part 2 of this article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Writing unit tests was the hardest part of this article. It required learning from multiple sources, comparing approaches, and deliberately settling on best practices rather than just copying the first example found. The learning curve is real, but it's worth it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;__init__.py&lt;/code&gt; files were confusing at first. It wasn't obvious why empty files were needed at all. Once the concept of "Python packages" clicked — that &lt;code&gt;__init__.py&lt;/code&gt; is what tells Python "treat this folder as importable" — everything fell into place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Routing in &lt;code&gt;app.py&lt;/code&gt; went through several iterations. The final clean structure didn't appear on the first try. It took a few rewrites before the cost optimization principle emerged: reject non-GET requests first, before any real work happens, to avoid unnecessary Lambda invocations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mapping Lambda to events in &lt;code&gt;template.yaml&lt;/code&gt; wasn't intuitive — understanding that &lt;code&gt;RestApiId: !Ref HelloWorldApi&lt;/code&gt; is what connects a route to your API Gateway (not a new one SAM creates automatically) took time to fully grasp.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Understanding why CORS headers matter on error responses was a hard-won insight. It feels counterintuitive at first. Why would a 404 need CORS? The answer: without it, the browser blocks the error entirely, and the client sees a confusing network failure instead of the actual error message.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A SAM template is a contract&lt;/strong&gt; — it describes the infrastructure your code needs, and SAM handles creating it. Understanding &lt;code&gt;template.yaml&lt;/code&gt; makes everything else — deployment, local testing, debugging — much less mysterious.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;mock_event()&lt;/code&gt; is the heart of unit testing a Lambda function&lt;/strong&gt; — once you understand that API Gateway just builds a dictionary and passes it to your function, testing becomes simple: build the dictionary yourself and check the output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated tests reveal what the console hides&lt;/strong&gt;: Console testing tells you whether something looks right. Automated tests tell you whether it is right including error messages, status codes, and headers that never appear on screen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write tests &lt;em&gt;while&lt;/em&gt; you build, not after&lt;/strong&gt; — tests written after the fact feel like chores. Tests written alongside the code feel like guardrails — you immediately see the value.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You’ve built a serverless REST API from scratch — a SAM template, a Lambda function with proper routing and error handling, a documentation endpoint, and a full unit test suite with 15 tests.&lt;br&gt;&lt;br&gt;
More importantly, you’ve developed a habit: write code → write tests → deploy with confidence. That loop is what separates code that &lt;em&gt;probably works&lt;/em&gt; from code you can &lt;em&gt;trust&lt;/em&gt;.&lt;br&gt;&lt;br&gt;
In Part 2, we’ll move from unit tests to integration tests — testing the real API Gateway, the real Lambda, and the real AWS infrastructure to make sure everything works end-to-end and final deployment to production AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.pytest.org/en/latest/getting-started.html#get-started" rel="noopener noreferrer"&gt;Pytest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://realpython.com/python-testing/" rel="noopener noreferrer"&gt;Getting Started With Testing in Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://catalog.workshops.aws/workshops/667d8814-18c9-4be9-bbc1-a6a3ffd2ba62/en-US/module-0-getting-started" rel="noopener noreferrer"&gt;The Complete AWS SAM Workshop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect with me on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/gloriaboakye/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@gloriaboakye" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Globak143" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
  </channel>
</rss>
