Build a Serverless Study Tracker API
Build a Python API with Lambda, API Gateway, DynamoDB, and CloudWatch.
Introduction
30 Second Summary
Study plans often end up scattered across notes, messages, and browser tabs. Once those records pile up, it becomes difficult to see where your study time went.
In this project, you will build a public Study Tracker API on AWS. It will save each study session so you can retrieve it through a live web endpoint.
What You'll Build
You'll watch a study session travel from Windows PowerShell through a live HTTPS endpoint before it appears in your saved session list.
By the end of this project, you'll have:
- A public Study Tracker API powered by AWS Lambda plus Amazon API Gateway. You can save sessions through POST /sessions before retrieving them through GET /sessions.
- A persistent study history stored in Amazon DynamoDB. Your sessions remain available between function invocations.
- A repeatable troubleshooting trail that recreates successful requests plus client errors from PowerShell. Amazon CloudWatch Logs connects each invocation with its route and runtime details.
- Secret Mission: Add DELETE /sessions/{id} so you can remove a stored study session through the API.
What do I need before starting?
You'll need a Windows device, an email address you can verify, and either Windows Hello or a mobile device running Microsoft Authenticator.
The project uses the AWS Management Console plus PowerShell, so you do not need a local Python installation or code editor.
Before We Start
Before you begin the hands-on work in AWS, take a moment to commit to what you are building and why it matters. Your goal sentence will connect each service to the architecture skills you want to practise.
Set Up and Secure Your AWS Project
Your future Study Tracker API will have a public endpoint. A protected account gives that workload a safe home.
An AWS Free Plan project keeps cost information visible. An AWS Builder ID protected by multi-factor authentication secures your access to the AWS Management Console.
In this step, get ready to:
- Create your AWS Free Plan project.
- Protect your AWS Builder ID with MFA.
- Verify your Windows PowerShell REST client.
Create your AWS Builder ID and project
The new AWS sign-up experience creates your AWS Builder ID first. It automatically provisions a project after you complete the account details.
AWS may place a temporary USD $1 verification hold for 3 to 5 days. Your Free Plan does not incur charges unless you upgrade.
- Press the Windows key to open search.
- Type Microsoft Edge.
- Press Enter to open Microsoft Edge.
- Open the official AWS account guide in Microsoft Edge.
- Choose Sign up for AWS (new).
- Enter an email address that you can verify.
- Submit the verification code that AWS emails to you.
- Create a password for your AWS Builder ID.
What happens after sign-up?
AWS uses your verified email address for the AWS Builder ID. The remaining sign-up prompts collect the contact details needed to provision your project.
The Free Plan lasts for six months or until its credits are exhausted. AWS does not charge the account unless you upgrade.
- Complete the requested contact details.
- Confirm that Cost and Usage shows your Free Plan credit information.
- Open the AWS Management Console from your provisioned project.
Your account is live. You can now see the Free Plan project that will contain every AWS resource in this build.
Protect your AWS Builder ID with MFA
MFA adds a second sign-in factor after your password. A built-in FIDO2 authenticator such as Windows Hello can use your device's biometric or PIN verification.
The Windows Hello path sits under Security key. AWS uses that option for built-in FIDO2 authenticators as well as physical security keys.
- Return to the Microsoft Edge window from earlier.
- Open your AWS Builder ID profile.
- Choose Security.
- Add a recovery email if AWS prompts you to provide one.
- Choose the tab that matches your device:
✔️ Windows Hello is available
- Choose Register device on the Security page.
- Choose Security key on the Register MFA device page.
- Follow the Windows security prompt shown by your browser.
- Approve the request with your configured Windows Hello method.
- Confirm that the new MFA device appears on the Security page.
ⓧ Windows Hello is unavailable
A TOTP authenticator app provides the fallback sign-in factor. Microsoft Authenticator generates a short-lived code that you enter during registration.
- Install Microsoft Authenticator from your phone's app store if needed.
- Choose Register device on the Security page.
- Choose Authenticator app on the Register MFA device page.
- Choose Show QR code.
- Scan the displayed code with Microsoft Authenticator.
- Enter the generated code in the Authenticator code box.
- Choose Assign MFA immediately.
- Confirm that the new MFA device appears on the Security page.
Your AWS Builder ID now has a second sign-in factor. That protection matters before you create an internet-facing API.
Verify your PowerShell REST client
Windows PowerShell includes Invoke-RestMethod for sending HTTP and HTTPS requests. You will use this cmdlet as the client for your Study Tracker API.
- Press the Windows key to open search.
- Type Windows PowerShell.
- Press Enter to open it.
Before you run the check, predict whether PowerShell will return cmdlet details.
- Check that the REST cmdlet is available by running this command:
Get-Command Invoke-RestMethod
What does this command do?
The Get-Command cmdlet asks PowerShell to locate a command. It returns metadata when Invoke-RestMethod is available.
You will see cmdlet details for Invoke-RestMethod. This confirms that PowerShell can act as your API client.
Can't find the REST cmdlet?
- Check that the window title says Windows PowerShell.
- Press the Windows key to reopen search.
- Type Windows PowerShell before pressing Enter.
Still missing the cmdlet? Help me verify Invoke-RestMethod in Windows PowerShell.
- Keep Windows PowerShell open for the API tests later in this project.
Your Setup Checkpoint
AWS Builder ID: Created, email verified, and protected by Windows Hello FIDO2 MFA or Microsoft Authenticator TOTP MFA.
AWS Free Plan project: Provisioned AWS Free Plan project with Cost and Usage information visible.
Windows PowerShell REST client: Windows PowerShell is open and Invoke-RestMethod is available.
Your AWS project is secured. Next up, you will run the first visible version of your backend in AWS Lambda.
Create Your First Lambda Function
A secured AWS project is the foundation for your application. You still need proof that your backend can execute code.
In this step, you'll use AWS Lambda to build the smallest visible version of the Study Tracker backend.
The function returns an online message after each test. Its logs leave a trace of the successful invocation.
In this step, get ready to:
- Create a Python 3.14 Lambda Event Function.
- Deploy a handler that returns the Study Tracker online message.
- Run an empty test event and inspect its invocation logs.
Create a Python Lambda Event Function
A Lambda Event Function runs your Python handler when it receives an event. AWS manages the underlying serverless compute environment.
An execution role is an IAM role that gives the function permission to use AWS services. Role manager supplies this role for your Free Plan project.
- Return to the AWS Management Console from earlier.
- Enter Lambda in the search bar at the top of the console.
- Select AWS Lambda from the search results.
- Choose Create Event Function.
- Select Author from scratch.
- Enter study-tracker-function in the Function name field.
- Choose Python 3.14 for Runtime.
- Keep the default execution role setting so role manager supplies the role.
What does the execution role do?
The supplied execution role lets the function send invocation records to Amazon CloudWatch Logs.
Those records show when the function started. They also show how long it ran.
- Choose Create Event Function.
That's the foundation in place: study-tracker-function now exists with its role-manager supplied execution role.
Add the study tracker handler
The Python runtime includes Boto3, which is the AWS SDK for Python. Your handler logs the included SDK version before returning a JSON response.
- Choose the Code tab on the function page.
- Select lambda_function.py in the file explorer.
- Select all of the starter code in the editor.
- Replace the starter code by pasting the following code:
import boto3
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("boto3=%s", boto3.__version__)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": "Study Tracker is online"}),
}
What does this code do?
- The boto3 import gives the handler access to the SDK included in the Lambda runtime.
- The json import converts the response body into a JSON string.
- The logging import provides the logger used by the function.
- The INFO level allows the Boto3 version entry to reach CloudWatch Logs.
- The lambda_handler() function is the entry point Lambda runs for each event.
- The returned dictionary carries status 200 with the online message in its response body.
- Choose Deploy in the DEPLOY section.
- Wait for the console to confirm the successful deployment.
Your deployed function now has a handler that returns the Study Tracker message. The same handler records the runtime's Boto3 version.
Deployment not completing?
- Confirm that lambda_function.py contains the complete code block.
- Check that the entry function is still named lambda_handler().
- Choose Deploy again after correcting the file.
Ask for help if the deployment still fails: Help me debug my Lambda deployment.
✔️ Awesome, I've got everything!
Your deployed lambda_function.py contains the complete handler for this step.
ⓧ I'd like to double check the full code
import boto3
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("boto3=%s", boto3.__version__)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": "Study Tracker is online"}),
}
Test the function and inspect logs
A Lambda event is a JSON document passed to the handler. An empty object is enough because this handler does not read any event fields.
- Choose the Test tab on the function page.
- Choose Create new event under Test event.
- Replace the existing event JSON by pasting the following code:
{}
Why use an empty event?
The empty object invokes the handler without supplying application data. This isolates the function response from the API routing logic you add later.
Before you run the test, picture the status you expect this empty event to return.
- Choose Test before saving to invoke the deployed function.
You should see status 200 with a response body containing Study Tracker is online.
- Expand Details under Execution result.
- Confirm that the response contains status 200.
- Confirm that the response body contains Study Tracker is online.
- Review Log output in the execution details.
- Confirm that the logs contain START.
- Confirm that the logs contain END.
- Confirm that the logs contain REPORT.
- Confirm that a custom log entry begins with boto3=.
What do the invocation logs prove?
The START entry marks the beginning of one invocation. The END entry marks its completion.
The REPORT entry records duration plus memory information. The boto3= entry confirms that your handler logged the runtime's included SDK version.
CloudWatch logs can take five to ten minutes to appear. A quiet log page during that window does not mean your test failed.
- Choose the Monitor tab on the function page.
- Choose View CloudWatch logs.
- Select the newest log stream in /aws/lambda/study-tracker-function.
You should see the successful invocation with START, END, REPORT, plus the custom boto3= entry.
Test not returning status 200?
- Return to the Code tab if the response still contains the starter message.
- Choose Deploy before running the test again.
- Check the logger.info line if the Boto3 version entry is missing.
- Run the test again if the CloudWatch log stream has not appeared after the wait.
Ask for help with the result: Help me debug my Lambda test.
Your first Lambda backend is live with a visible response and traceable logs. Next up, you'll place an HTTP API in front of it.
Publish Your HTTP API
Your AWS Lambda function now returns a successful response from a console test. The function still needs a public address before an application can call it.
Amazon API Gateway gives your function an HTTPS endpoint. In this step, you'll publish GET /sessions through an HTTP API and compare how two request methods behave.
In this step, get ready to:
- Create an HTTP API with an automatically deployed stage.
- Connect the GET /sessions route to study-tracker-function.
- Call the API from Windows PowerShell with GET and POST requests.
Create the HTTP API shell
The AWS Management Console wizard creates the API with a stage named $default. Automatic deployment makes later route changes available without a separate deployment action.
- In the AWS Management Console from earlier, enter API Gateway in the top search box.
- Select API Gateway from the search results.
You'll see the available API types. HTTP API is the lightweight option used for this project.
- Under HTTP API, choose Build.
- Enter study-tracker-api in the API name field.
- Choose Next.
You'll see the Configure routes page. The route is easier to configure after the API shell exists.
- Choose Next to skip route creation.
The stage review should list $default with automatic deployment. This stage serves requests from the API's base invoke URL.
- Confirm that the stage is $default.
- Choose Next.
- Choose Create.
You'll land on the API's Details page. The page identifies your new study-tracker-api and its invoke URL.
API not showing after creation?
- Confirm that the APIs list contains study-tracker-api.
- Refresh the console once if the Details page does not load.
Ask for help with the API creation flow.
Connect the GET route to Lambda
A route combines an HTTP method with a resource path. The GET /sessions route tells API Gateway which requests should invoke your existing Lambda function.
- Choose Routes in the navigation pane.
- Choose Create.
- Choose GET for Method.
- Enter /sessions for the path.
- Choose Create.
You'll see GET /sessions in the route list. The API now recognizes that exact method and path combination.
- Choose Integrations in the navigation pane.
- Choose Manage integrations.
- Choose Create.
The integration form connects an API route to a backend resource. Your backend is the Lambda function from the previous step.
- Leave Attach this integration to a route unselected.
- Choose Lambda function for Integration type.
The form now displays the field for selecting an existing Lambda function.
- Enter study-tracker-function in the Lambda function field.
- Choose Create.
You'll see study-tracker-function in the integration list. The final connection assigns that integration to your route.
- In Integrations, select GET /sessions.
The route panel offers the Lambda integration you just created.
- Under Choose an existing integration, choose study-tracker-function.
- Choose Attach integration.
You'll see the function attached to GET /sessions. Automatic deployment publishes the connection through the $default stage.
Route not connected?
- Check that the selected route is GET /sessions.
- Repeat Attach integration if the route does not show study-tracker-function.
Help me connect this route to Lambda.
Test GET and POST from PowerShell
The invoke URL identifies your public API. Windows PowerShell adds /sessions to that base URL when it calls the route.
- Choose Details in the API navigation pane.
- Copy the value shown under Invoke URL.
- Save the copied value here: your study-tracker-api invoke URL.
- Return to Windows PowerShell from earlier.
- Set $apiUrl and send the GET request by running:
$apiUrl = "[[API_URL=\"your study-tracker-api invoke URL\"]]"
Invoke-RestMethod -Method Get -Uri "$apiUrl/sessions"
What do these commands do?
- The first line stores your API's base invoke URL in $apiUrl so later requests can reuse it.
- The second line sends a GET request to GET /sessions through API Gateway.
You'll see the message Study Tracker is online. Your public route now reaches the deployed Lambda function.
GET request not returning the message?
- Check that $apiUrl contains the copied invoke URL.
- Confirm that GET /sessions shows study-tracker-function as its integration.
Help me debug this GET request.
The API currently has one explicit route. Take a moment to predict whether changing only the request method will still reach Lambda.
- Build a JSON study session and send it with a POST request by running:
$body = @{ id = "s1"; topic = "AWS Lambda"; minutes = 30 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$apiUrl/sessions" -Body $body -ContentType "application/json"
What does this request send?
- The first line converts the study session fields into a JSON request body stored in $body.
- The second line sends that body to /sessions with the POST method and a JSON content type.
You'll see 404 Not Found. This is the intended shortfall because POST /sessions is absent from the route table.
Why did GET work while POST failed?
API Gateway matches both the HTTP method and the resource path. GET /sessions matches your configured route.
The POST request uses the same path with a different method. API Gateway rejects that unmatched request before it reaches Lambda.
POST request did not return 404?
- Confirm that the route list contains GET /sessions without POST /sessions.
- Confirm that the PowerShell request uses -Method Post.
- Remove any unintended $default route that forwards unmatched requests to Lambda.
Help me reproduce the missing-route response.
Your public GET route now reaches Lambda. Next, you'll add the missing write route and store study sessions in Amazon DynamoDB.
Persist Study Sessions
The missing POST route proved that Amazon API Gateway only accepts routes you create. Your Study Tracker now needs a place that keeps sessions after each AWS Lambda invocation ends.
Amazon DynamoDB provides durable storage without a database server to manage. You will connect the function to a table before completing the API write path.
In this step, get ready to:
- Create a DynamoDB table for study sessions.
- Connect the Lambda function to the table.
- Complete the API write path through a POST route.
Create the DynamoDB table
Each DynamoDB item needs a partition key that identifies it. Your table uses each session's id value for that key.
- Click the service search bar in the AWS Management Console.
- Enter DynamoDB in the search bar.
- Select DynamoDB from the results.
- Select Tables in the left navigation panel.
- Click Create table.
- Enter study-sessions in the table name field.
- Enter id in the partition key field.
- Select String as the partition key type.
How the partition key works
DynamoDB uses the partition key to locate an item. A session with an id of s1 can be stored under that identifier.
The default table settings provide enough capacity for this tutorial-scale workload.
- Leave the table settings at their default values.
- Click Create table.
- Confirm that study-sessions is ready for use in the tables list.
Good progress. Your API now has a durable home for every study session it receives.
Table not ready?
Check that the table name is study-sessions. Confirm that the partition key is id with the String type.
Ask for help with the table configuration:
Connect Lambda to the table
An environment variable gives the function its table name through configuration. The code can then create a Boto3 table resource from that value.
- Return to study-tracker-function in AWS Lambda.
- Select the Configuration tab.
- Select Environment variables.
- Click Edit.
- Click Add environment variable.
- Enter TABLE_NAME in the key field.
- Enter study-sessions in the value field.
- Click Save.
The environment variables list now shows TABLE_NAME with the value study-sessions.
- Select the Code tab.
- Select lambda_function.py in the code source panel.
- Replace the existing starter code with this first chunk:
import json
import logging
import os
from decimal import Decimal
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
TABLE_NAME = os.environ["TABLE_NAME"]
table = boto3.resource("dynamodb").Table(TABLE_NAME)
def decimal_to_number(value):
if isinstance(value, Decimal):
if value == value.to_integral_value():
return int(value)
return float(value)
raise TypeError(f"{type(value).__name__} is not JSON serializable")
What does this code do?
- The imports provide JSON handling, logging, environment access, decimal numbers, and the AWS SDK.
- TABLE_NAME reads the table name from the function configuration.
- table represents the DynamoDB table used by each request.
- decimal_to_number() converts DynamoDB decimal values into numbers that JSON can serialize.
- Add this next chunk directly below decimal_to_number() in lambda_function.py:
def make_response(status_code, body):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body, default=decimal_to_number),
}
def lambda_handler(event, context):
route_key = event.get("routeKey", "")
logger.info("route=%s boto3=%s", route_key, boto3.__version__)
if route_key == "GET /sessions":
result = table.scan()
return make_response(200, {"sessions": result.get("Items", [])})
if route_key == "POST /sessions":
payload = json.loads(event.get("body") or "{}")
session = {
"id": payload["id"],
"topic": payload["topic"],
"minutes": Decimal(str(payload["minutes"])),
}
table.put_item(Item=session)
return make_response(201, {"message": "Session saved", "session": session})
return make_response(404, {"message": "Route not found"})
How does the handler use each route?
- make_response() gives every response the same status, content type, and serialized body structure.
- route_key identifies the HTTP method plus path that reached Lambda.
- GET /sessions scans the table before returning its stored items.
- POST /sessions creates a session from the request body before storing it with table.put_item().
- Route not found covers any route the handler does not recognize.
- Click Deploy above the code editor.
- Predict whether the updated GET route returns the old online message or a sessions collection.
- Return to Windows PowerShell from earlier.
- Check the updated GET route by running this command:
Invoke-RestMethod -Method Get -Uri "$apiUrl/sessions"
What should you see?
The command sends an HTTPS GET request to the existing route. You should see an empty sessions collection because the new table has no stored items yet.
Still seeing the online message?
Confirm that you clicked Deploy after adding both code chunks. Check that TABLE_NAME still contains study-sessions under the function's environment variables.
Ask for help tracing the function configuration:
Your deployed lambda_function.py should now match the complete persistence version below.
✔️ Awesome, I've got everything!
Your function is configured and deployed. Continue to the route setup.
ⓧ I'd like to double check the full code
import json
import logging
import os
from decimal import Decimal
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
TABLE_NAME = os.environ["TABLE_NAME"]
table = boto3.resource("dynamodb").Table(TABLE_NAME)
def decimal_to_number(value):
if isinstance(value, Decimal):
if value == value.to_integral_value():
return int(value)
return float(value)
raise TypeError(f"{type(value).__name__} is not JSON serializable")
def make_response(status_code, body):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body, default=decimal_to_number),
}
def lambda_handler(event, context):
route_key = event.get("routeKey", "")
logger.info("route=%s boto3=%s", route_key, boto3.__version__)
if route_key == "GET /sessions":
result = table.scan()
return make_response(200, {"sessions": result.get("Items", [])})
if route_key == "POST /sessions":
payload = json.loads(event.get("body") or "{}")
session = {
"id": payload["id"],
"topic": payload["topic"],
"minutes": Decimal(str(payload["minutes"])),
}
table.put_item(Item=session)
return make_response(201, {"message": "Session saved", "session": session})
return make_response(404, {"message": "Route not found"})
Add the POST route and test persistence
The handler understands POST /sessions, but API Gateway still needs a matching route. Attaching the existing integration lets the public endpoint send those requests to the same function.
- Return to study-tracker-api in Amazon API Gateway.
- Select Routes in the left navigation panel.
- Click Create.
- Select POST as the method.
- Enter /sessions as the path.
- Select study-tracker-function as the existing Lambda integration.
- Click Create.
- Confirm that POST /sessions appears beside the existing GET route.
The write route is now live through the automatically deployed stage. Start with a complete session to prove the storage path works.
- Create the session body before sending it to the POST route by running these commands:
$body = @{ id = "s1"; topic = "AWS Lambda"; minutes = 30 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$apiUrl/sessions" -Body $body -ContentType "application/json"
What do these commands do?
- ConvertTo-Json turns the PowerShell hashtable into the JSON request body.
- Invoke-RestMethod sends that body to POST /sessions with the JSON content type.
- The response should contain Session saved plus the stored session.
Still seeing a missing route response?
Confirm that the route method is POST. Check that its path is exactly /sessions.
Ask for help checking the integration:
- Predict what the GET route returns now that the POST request has completed.
- List the stored sessions by running this command:
Invoke-RestMethod -Method Get -Uri "$apiUrl/sessions"
What should you see now?
The response should include a session with s1 as its ID. It should also show AWS Lambda as its topic plus 30 as its minutes value.
That's the storage loop working. Your public API can now save a session before reading it back from DynamoDB.
A useful API also needs to face incomplete input. You will now send a session without topic to see how this first implementation classifies it.
- Predict whether the incomplete request produces a client response or a server response.
- Send the incomplete session by running these commands:
$body = @{ id = "s2"; minutes = 20 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$apiUrl/sessions" -Body $body -ContentType "application/json"
The 500 response is intentional
The API reports a 500 server response because the code directly reads the missing topic field. The server has classified incomplete client data as an internal failure.
This shortfall exposes the need for defensive request validation. The next step turns this case into a useful client response.
Your Study Tracker now stores complete sessions through its public API. Next, you will validate every request and trace the resulting routes through the function logs.
Validate Requests and Trace Logs
Your valid session in Amazon DynamoDB proves that the API can persist data. The earlier missing topic produced a server error because the handler trusted every request body.
A missing client field should produce a clear client response. In this step, you will add defensive JSON validation to the AWS Lambda handler. You will then use Amazon CloudWatch Logs to trace each route.
In this step, get ready to:
- Replace the naive request handling with defensive validation.
- Test the updated POST route across failure and success cases.
- Trace recent API requests through the Lambda logs.
Add defensive request validation
The current handler directly reads every required field. A validation helper can identify missing values before the request reaches DynamoDB.
- Return to study-tracker-function in the AWS Lambda console.
- Select lambda_function.py in the console editor.
- Find def lambda_handler(event, context): near the middle of the file.
- Place your cursor on the blank line immediately above lambda_handler().
- Add the validation helper by pasting this code:
def validate_session(payload):
required_fields = ("id", "topic", "minutes")
missing_fields = [field for field in required_fields if field not in payload]
if missing_fields:
return f"Missing required fields: {', '.join(missing_fields)}"
if not isinstance(payload["id"], str) or not payload["id"].strip():
return "id must be a non-empty string"
if not isinstance(payload["topic"], str) or not payload["topic"].strip():
return "topic must be a non-empty string"
if type(payload["minutes"]) not in (int, float) or payload["minutes"] <= 0:
return "minutes must be a positive number"
return None
What does this helper check?
- The required_fields tuple defines the three values that every session needs.
- The missing_fields list records any required keys that are absent.
- The string checks reject blank id or topic values.
- The numeric check requires minutes to be greater than zero.
- Find the existing POST block that begins with if route_key == "POST /sessions":.
- Replace that entire POST block with this validated version:
if route_key == "POST /sessions":
try:
payload = json.loads(event.get("body") or "{}")
except json.JSONDecodeError:
return make_response(400, {"message": "Request body must be valid JSON"})
validation_error = validate_session(payload)
if validation_error:
return make_response(400, {"message": validation_error})
session = {
"id": payload["id"].strip(),
"topic": payload["topic"].strip(),
"minutes": Decimal(str(payload["minutes"])),
}
table.put_item(Item=session)
return make_response(
201,
{"message": "Session saved", "session": session},
)
How does the POST route respond?
- The try block parses the request body as JSON.
- The except json.JSONDecodeError block turns malformed JSON into a 400 response.
- The validate_session() call checks the request before any data is written.
- The strip() calls remove surrounding whitespace from stored text.
- Publish the updated handler by choosing Deploy in the Lambda console.
The console confirms that your changes are deployed. The next API request now runs the validated handler.
Deployment not completing?
Confirm that validate_session() sits above lambda_handler(). Check that the replaced POST block remains indented inside the handler.
Use this if the console reports a Python syntax problem: Help me check my Lambda validation code for indentation or syntax errors.
✔️ Awesome, I've got everything!
Your validated handler is deployed. Continue to the PowerShell tests.
ⓧ I'd like to double check the full code
Compare your deployed lambda_function.py with this complete version.
import json
import logging
import os
from decimal import Decimal
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
TABLE_NAME = os.environ["TABLE_NAME"]
table = boto3.resource("dynamodb").Table(TABLE_NAME)
def decimal_to_number(value):
if isinstance(value, Decimal):
if value == value.to_integral_value():
return int(value)
return float(value)
raise TypeError(f"{type(value).__name__} is not JSON serializable")
def make_response(status_code, body):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body, default=decimal_to_number),
}
def validate_session(payload):
required_fields = ("id", "topic", "minutes")
missing_fields = [field for field in required_fields if field not in payload]
if missing_fields:
return f"Missing required fields: {', '.join(missing_fields)}"
if not isinstance(payload["id"], str) or not payload["id"].strip():
return "id must be a non-empty string"
if not isinstance(payload["topic"], str) or not payload["topic"].strip():
return "topic must be a non-empty string"
if type(payload["minutes"]) not in (int, float) or payload["minutes"] <= 0:
return "minutes must be a positive number"
return None
def lambda_handler(event, context):
route_key = event.get("routeKey", "")
logger.info("route=%s boto3=%s", route_key, boto3.__version__)
if route_key == "GET /sessions":
result = table.scan()
return make_response(200, {"sessions": result.get("Items", [])})
if route_key == "POST /sessions":
try:
payload = json.loads(event.get("body") or "{}")
except json.JSONDecodeError:
return make_response(400, {"message": "Request body must be valid JSON"})
validation_error = validate_session(payload)
if validation_error:
return make_response(400, {"message": validation_error})
session = {
"id": payload["id"].strip(),
"topic": payload["topic"].strip(),
"minutes": Decimal(str(payload["minutes"])),
}
table.put_item(Item=session)
return make_response(
201,
{"message": "Session saved", "session": session},
)
return make_response(404, {"message": "Route not found"})
Test rejected and accepted requests
A good API gives the caller enough information to correct a bad request. Your open Windows PowerShell window lets you test that boundary from outside the AWS console.
- Create a request body without topic by running:
$body = @{ id = "s2"; minutes = 20 } | ConvertTo-Json
What does this body test?
The body contains a valid id plus a positive minutes value. The missing topic isolates the validation rule you just added.
Before you send this request, do you expect the API to report a server failure or a client error?
- Send the incomplete session to the POST route by running:
Invoke-RestMethod -Method Post -Uri "$apiUrl/sessions" -Body $body -ContentType "application/json"
What should I see?
PowerShell reports status 400 with the message Missing required fields: topic. The response now identifies a request problem instead of treating it as a server failure.
That 400 response is the key improvement. Your API now gives callers a specific correction they can make.
Still seeing a server error?
Confirm that you chose Deploy after updating the handler. Check that the POST route calls validate_session(payload) before creating session.
Use this if the old behavior continues: Help me find why my Lambda POST route still returns a server error for a missing topic.
- Create a complete study session body by running:
$body = @{ id = "s1"; topic = "AWS Lambda"; minutes = 30 } | ConvertTo-Json
Why should this body pass?
This body includes every field in required_fields. Its text values are non-empty. Its minutes value is positive.
- Save the complete session through the API by running:
Invoke-RestMethod -Method Post -Uri "$apiUrl/sessions" -Body $body -ContentType "application/json"
What should I see?
The API returns Session saved with the stored session. The successful response uses status 201.
- List the persisted sessions through the GET route by running:
Invoke-RestMethod -Method Get -Uri "$apiUrl/sessions"
What should I see?
The response contains the stored session with id set to s1. The value remains available after the separate POST invocation.
Trace requests in CloudWatch Logs
API responses show what the caller received. Lambda logs show which route ran plus which runtime library handled the request.
Before you inspect the newest stream, which route entries do you expect from the requests you just sent?
- Return to the AWS Management Console browser tab.
- Return to study-tracker-function in AWS Lambda.
- Return to the function's existing log group at /aws/lambda/study-tracker-function.
- Select the newest log stream.
- Find the recent route=POST /sessions entries.
- Find the recent route=GET /sessions entry.
- Find the surrounding START, END, and REPORT lines.
You will see both route values beside boto3= version output. The runtime report also contains duration plus memory information for each invocation.
How do these logs help?
The route= entries come from your application logger. They connect each invocation to the API behavior that triggered it.
The runtime adds the invocation boundaries plus the execution report. Together, these lines show what ran and how the invocation behaved.
Can't find the recent route logs?
Refresh the existing log group after sending another GET request. Select the newest stream because older streams may contain invocations from earlier code versions.
Use this if the application log lines remain missing: Help me trace why my Lambda route logs are missing from the newest CloudWatch log stream.
You have turned the naive server failure into a useful client response. Your API now stores validated sessions and leaves a trace for every tested route.
Secret mission
Delete a Study Session
Your Study Tracker can create and list sessions. Add a DELETE route that reads an id from the URL. Use it to remove the matching item. Trace the request in CloudWatch Logs.
Clean Up Your Resources
Clean Up Your Resources
Tutorial-scale use is expected to cost $0 while your account remains on the AWS Free Plan. Your next choice is to keep the resources available, pause public access, or delete the workload.
Why Cleanup Still Matters
Your public HTTP API remains available for requests while it exists. Keeping it online leaves its routes exposed to internet traffic.
Lambda log groups persist after their functions are deleted. The CloudWatch Logs log group needs its own cleanup step.
Resources you used:
- Amazon API Gateway HTTP API study-tracker-api.
- AWS Lambda Event Function study-tracker-function.
- Amazon DynamoDB table study-sessions.
- CloudWatch Logs log group /aws/lambda/study-tracker-function.
- Role-manager execution role supplied to study-tracker-function.
Keep everything running
No action is needed. Choose this option if you want to continue testing your Study Tracker API.
- Review the Cost and Usage information in your AWS Free Plan project.
- Keep study-tracker-api available for future API tests.
- Keep your saved study sessions in study-sessions.
- Use only non-sensitive tutorial data while the public endpoint remains available.
Pause - I'll come back to this later
Pause public access by deleting only the HTTP API. Your Lambda function remains available for a future API rebuild.
- Return to Amazon API Gateway in the AWS Management Console.
- Select study-tracker-api from the API list.
- Choose the deletion control on the API details page.
- Confirm the deletion of study-tracker-api.
The invoke URL stored in $apiUrl no longer accepts requests. Your Lambda function remains connected to the data in study-sessions.
Your existing CloudWatch logs also remain available for reference.
Delete - I don't want to use this again
Remove the complete Study Tracker workload from your AWS Free Plan project. This option deletes the endpoint, stored sessions, function, logs, and generated execution role.
Deletion Is Permanent
Deleting these resources permanently removes the saved study sessions and invocation history. You have already proved the workload works.
Your AWS Builder ID remains available for future projects. Its MFA registration also stays in place.
Remove the Public Endpoint
- Return to Amazon API Gateway in the AWS Management Console.
- Select study-tracker-api from the API list.
- Choose the deletion control on the API details page.
- Confirm the deletion of study-tracker-api.
The invoke URL stored in $apiUrl no longer accepts requests.
Remove the Stored Sessions
- Return to Amazon DynamoDB in the AWS Management Console.
- Select study-sessions from the table list.
- Choose the deletion control on the table details page.
- Confirm the deletion of study-sessions.
The table deletion removes every persisted study session.
Remove the Lambda Function
This part is a little fiddly because the execution role has a generated name. The function provides the reliable link to the correct role.
- Return to AWS Lambda in the AWS Management Console.
- Select study-tracker-function from the function list.
- Open the supplied execution role from the function configuration page in a separate browser tab.
- Switch back to the browser tab for study-tracker-function.
- Choose the deletion control on the function details page.
- Confirm the deletion of study-tracker-function.
The Lambda function can no longer process study-session requests.
Remove the Log History
- Return to Amazon CloudWatch Logs in the AWS Management Console.
- Select /aws/lambda/study-tracker-function from the log group list.
- Choose the deletion control on the log group page.
- Confirm the deletion of /aws/lambda/study-tracker-function.
The GET, POST, and DELETE route logs are now removed.
Remove the Execution Role
- Switch to the browser tab containing the execution role supplied to study-tracker-function.
- Choose the deletion control on the role details page.
- Confirm the deletion of the generated execution role.
Cleanup complete. Your Study Tracker workload no longer has a public endpoint.
What Stays After Cleanup
Your AWS Builder ID remains available for future projects. Your MFA registration stays attached to it.
The AWS Free Plan project also remains available for your next workload.
Nice Work!
Nice Work!
You made it! Your protected AWS project now runs a public Study Tracker API that manages persisted sessions.
What you learned:
- Published HTTPS routes through Amazon API Gateway that invoke AWS Lambda for each request.
- Added request validation for JSON bodies before each Amazon DynamoDB write.
- Traced route activity in Amazon CloudWatch Logs with the Boto3 version plus invocation duration and memory details.
- Secret Mission: Added DELETE /sessions/{id} so the API removes a stored session through DynamoDB.
Ready to quiz yourself?