Serverless Student Assignment Tracker
Build a REST API on AWS with Lambda, DynamoDB, and API Gateway.
Introduction
30 Second Summary
Every student juggles deadlines across multiple courses. Imagine having a personal API that tracks all your assignments, tells you what's due this week, and filters by course or completion status.
In this project, you will build a serverless REST API on AWS that handles full CRUD operations for student assignments. You'll design a DynamoDB table, write a Lambda function in Python, and connect everything through API Gateway to create a live, publicly accessible endpoint.
What You'll Build
A Student Assignment Tracker API that supports creating, reading, updating, and deleting assignments, plus filtering by course and querying upcoming deadlines.
By the end of this project, you'll have:
- A DynamoDB table with a composite primary key and a Global Secondary Index designed for real access patterns.
- A Lambda function in Python that handles routing, CRUD operations, and date-based filtering logic.
- A live API Gateway endpoint you can invoke from anywhere with curl or Postman.
- Secret Mission: Add a priority filter and a stats endpoint that returns assignment completion percentages per course.
Are there any prerequisites?
You'll need an AWS account on the Free Plan (sign-up gives you $200 in credits with zero charges for 6 months). Basic Python knowledge is helpful but not required.
Set Up Your AWS Account
Before you can build anything on AWS, you need an account and access to the Management Console.
AWS offers a Free Plan for new accounts that includes $200 in credits and zero charges for 6 months. Everything in this project uses always-free services, so you will not be charged.
In this step, get ready to:
- Create a free AWS account.
- Set your console region to us-east-1.
- Verify access to Lambda, DynamoDB, and API Gateway.
Create a free AWS account
The AWS Free Plan gives new customers $100 in credits on sign-up, plus up to $100 more by completing activities. The plan lasts 6 months with no charges.
- Open your web browser and navigate to https://aws.amazon.com/free/.
- Click Create a Free Account (or Complete Sign Up if you see that option).
- Enter your email address and choose an AWS account name.
- Complete the verification steps (email confirmation, payment method, phone verification).
- When prompted to choose a support plan, select the Free plan (also called Basic Support).
Why do I need a payment method?
AWS requires a credit or debit card on file for identity verification. You will not be charged while on the Free Plan, and everything in this project uses always-free services.
Already have an AWS account?
If you already have an AWS account, skip ahead to signing in below. Your existing account can use the always-free tier for Lambda and DynamoDB regardless of when it was created.
Having trouble creating your account? Help me troubleshoot AWS account creation
Sign in and set your region
AWS resources live in specific geographic regions. Setting your region ensures all the services you create in this project end up in the same place.
- Navigate to https://console.aws.amazon.com/ and sign in with your new account credentials.
- Look at the top navigation bar. Find the region selector dropdown (it displays a region name like US East (N. Virginia) or a region code).
- Click the region selector dropdown.
- Select US East (N. Virginia) which has the code us-east-1.
Why us-east-1?
The us-east-1 region (N. Virginia) has the widest service availability of any AWS region. It supports all the services you need for this project and is the default for many AWS tutorials.
Confirm that the navigation bar now shows US East (N. Virginia) or us-east-1 as your selected region.
Verify access to the core services
This project uses three AWS services together. Confirming you can find them now saves troubleshooting later.
- Click the search bar at the top of the console (or press Alt+S).
- Type Lambda and click Lambda in the search results to open the Lambda console.
- Confirm you see the Lambda dashboard (it shows a Functions section, even if empty).
- Click the search bar again and type DynamoDB.
- Click DynamoDB in the results to open the DynamoDB console.
- Confirm you see the DynamoDB dashboard (it shows a Tables section).
- Click the search bar one more time and type API Gateway.
- Click API Gateway in the results to open the API Gateway console.
- Confirm you see the API Gateway landing page.
What are these three services?
Lambda runs your code without managing servers. You write a function, and AWS handles everything else.
DynamoDB is a NoSQL database that stores your assignment data. It scales automatically and has an always-free tier.
API Gateway creates the HTTP endpoints that connect the outside world to your Lambda function. It turns your function into a REST API anyone can call.
Can't find a service in the search results?
Make sure your region is set to us-east-1. Some services are not available in all regions.
If search returns no results, try refreshing the page and searching again. Help me find AWS services in the console
Your AWS environment is ready. Next up, you will design and create the DynamoDB table that stores all your assignment data.
Create the DynamoDB Table
Your AWS account is ready. Now it's time to build the database that will store all your assignment data.
Every API needs a place to persist data. For this project, you'll use DynamoDB, a NoSQL database service from AWS that stores items in tables without requiring a fixed schema. You'll design your table's key structure around real access patterns, like querying all assignments for a student or filtering by course.
In this step, get ready to:
- Create a DynamoDB table with a composite primary key.
- Add a Global Secondary Index for course-based queries.
- Insert test items to verify the table works.
Navigate to DynamoDB and create the table
Your table needs a primary key that uniquely identifies each item. You'll use a composite primary key with two parts: a partition key that groups related items together, and a sort key that distinguishes items within that group.
- In the AWS Management Console, type DynamoDB in the top search bar and select the DynamoDB service.
- In the left navigation pane, click **Tables**.Tables
- Click Create table.
- For Table name, enter StudentAssignments.
- For Partition key, enter student_id and keep the type as String.
- For Sort key, enter assignment_id and keep the type as String.
- Under Table settings, select Customize settings.
- In the Read/write capacity settings section, select On-demand as the capacity mode.
Why on-demand capacity mode?
On-demand mode means DynamoDB automatically scales to handle your read and write traffic. You only pay for the requests you make. This is ideal for a learning project where traffic is unpredictable and low.
The alternative (provisioned mode) requires you to pre-set capacity limits. On-demand is simpler and still covered by your Free Plan credits.
Add a Global Secondary Index
Your table's primary key lets you query by student_id. But what if you want to find all assignments for a specific course? That requires a different access pattern. A Global Secondary Index (GSI) creates a separate lookup structure with its own key, enabling efficient queries on attributes that are not part of the primary key.
- Scroll down to the Secondary indexes section on the same Create table page.
- Click Create global index.
- For Partition key, enter course_code with type String.
- For Sort key, enter due_date with type String.
- For Index name, enter CourseIndex.
- For Attribute projections, select All.
- Click Create index.
- Scroll to the bottom of the page and click Create table.
What does this GSI enable?
The CourseIndex GSI lets you efficiently query all assignments for a specific course code (like CS601), sorted by due date. Without it, you would need to scan the entire table and filter results, which is slow and expensive at scale.
This is a classic DynamoDB design pattern. You model your indexes around the questions your application needs to answer.
Wait for the table status to change from Creating to Active. This usually takes a few seconds.
Table stuck in Creating status?
It typically takes 5-10 seconds for the table to become active. If it takes longer than a minute, refresh the page and check the table's status in the Tables list. Help me troubleshoot my DynamoDB table creation
Also confirm your region is set to us-east-1 in the top-right corner of the console. Creating the table in a different region means it won't be found by your Lambda function later.
Add test items to verify the table
Before you write any code, confirm that your table accepts data by manually inserting a few items through the console.
- Click the StudentAssignments table name to open it.
- Click Explore table items.
- Click Create item.
- For student_id, enter student-001.
- For assignment_id, enter assign-001.
- Click Add new attribute, choose String, and enter the attribute name course_code with value CS601.
- Click Add new attribute, choose String, and enter the attribute name title with value Final Project Proposal.
- Click Add new attribute, choose String, and enter the attribute name due_date with value 2026-06-01.
- Click Add new attribute, choose String, and enter the attribute name status with value incomplete.
- Click Create item.
You should see your new item appear in the Items returned section of the page.
- Repeat the process to create a second item with these values: student_id = student-001, assignment_id = assign-002, course_code = CS602, title = Lab Report 3, due_date = 2026-05-25, status = incomplete.
You should now see two items listed in the table. Both items share the same student_id but have different assignment_id values.
Why a composite primary key?
The partition key student_id groups all assignments for a student together. The sort key assignment_id makes each assignment unique within that group.
This design means you can query a single student's assignments efficiently without scanning the entire table. DynamoDB retrieves all items that share the same partition key in one fast operation.
Don't see your items?
Make sure you clicked Create item (not just filled in the fields). Check the Items returned section below the query builder. If you see a scan filter applied, click Run to re-run the scan with no filters. Help me find my items in DynamoDB
Also verify you are viewing the correct table. The table name must be exactly StudentAssignments (case-sensitive).
Your database is live and accepting data. Next up, you'll create the Lambda function that reads and writes to this table through code.
Create the Lambda Function
Your DynamoDB table stores the data, but you need compute logic to process API requests and perform the right operation for each one.
In this step, you'll create an AWS Lambda function that reads incoming HTTP requests, performs the matching DynamoDB operation, and returns formatted responses. This single function handles all the API logic for creating, reading, updating, and deleting assignments.
In this step, get ready to:
- Create a Lambda function with DynamoDB access permissions.
- Write the full assignment tracker code in the Lambda editor.
- Test the function with sample API events.
Create the Lambda function and attach DynamoDB permissions
Lambda needs permission to read and write to your DynamoDB table. You'll create the function first, then add a managed policy that grants full DynamoDB access to its execution role.
- Type Lambda in the AWS console search bar at the top and select Lambda from the results.
- Click Create function.
- Select Author from scratch.
- Enter AssignmentTrackerFunction as the function name.
- For Runtime, choose Python 3.13.
- Expand Change default execution role and select Create a new role with basic Lambda permissions.
- Click Create function.
Lambda creates your function and a new IAM execution role. Now you need to give that role permission to access DynamoDB.
- Scroll down to the Configuration tab and click Permissions in the left sidebar.
- Under Execution role, click the role name link. It looks something like AssignmentTrackerFunction-role-xxxxxxxx. This opens the IAM console in a new tab.
- In the IAM console, on the Permissions tab, click Add permissions and then choose Attach policies.
- In the search box, type AmazonDynamoDBFullAccess and press Enter.
- Select the checkbox next to AmazonDynamoDBFullAccess and click Add permissions.
- Close the IAM tab and return to the Lambda console tab.
Why AmazonDynamoDBFullAccess?
This AWS managed policy gives your Lambda function full read and write access to all DynamoDB tables. For a learning project, this is the fastest way to get running.
In production, you would create a custom policy scoped to just the StudentAssignments table ARN. This is a security best practice worth mentioning in interviews.
Don't see the role link?
Make sure you're on the Configuration tab (not the Code tab), then click Permissions in the left sidebar. The role name appears as a clickable blue link under Execution role.
help me find the execution role for my Lambda function
Write the assignment tracker code
The Lambda runtime already includes boto3 (the AWS SDK for Python), so you don't need to install any dependencies. You'll replace the default function code with the full assignment tracker logic.
- In the Lambda console, click the Code tab to open the code editor.
- Select all the existing code in lambda_function.py and delete it.
- Paste the following code into the editor:
import json
import boto3
import uuid
from datetime import datetime, timedelta, timezone
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('StudentAssignments')
def lambda_handler(event, context):
http_method = event.get('httpMethod', '')
path = event.get('path', '/')
query_params = event.get('queryStringParameters') or {}
try:
if path == '/assignments' and http_method == 'POST':
return create_assignment(event)
elif path == '/assignments' and http_method == 'GET':
return get_assignments(query_params)
elif path.startswith('/assignments/') and http_method == 'PATCH':
assignment_id = path.split('/')[-1]
return update_assignment(assignment_id, event)
elif path.startswith('/assignments/') and http_method == 'DELETE':
assignment_id = path.split('/')[-1]
return delete_assignment(assignment_id, query_params)
elif path == '/assignments/due-soon' and http_method == 'GET':
return get_due_soon(query_params)
else:
return build_response(404, {'message': 'Not found'})
except Exception as e:
return build_response(500, {'message': str(e)})
def create_assignment(event):
body = json.loads(event.get('body', '{}'))
# Build the assignment item with a unique ID and default values
item = {
'student_id': body['student_id'],
'assignment_id': str(uuid.uuid4()),
'course_code': body['course_code'],
'title': body['title'],
'due_date': body['due_date'],
'priority': body.get('priority', 'medium'),
'status': 'incomplete',
'created_at': datetime.now(timezone.utc).isoformat()
}
table.put_item(Item=item)
return build_response(201, item)
def get_assignments(query_params):
student_id = query_params.get('student_id')
course_code = query_params.get('course_code')
# Query by course using the GSI, or by student using the primary key
if course_code:
response = table.query(
IndexName='CourseIndex',
KeyConditionExpression=boto3.dynamodb.conditions.Key('course_code').eq(course_code)
)
elif student_id:
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('student_id').eq(student_id)
)
else:
return build_response(400, {'message': 'Provide student_id or course_code query parameter'})
items = response.get('Items', [])
# Optional client-side filter by completion status
status_filter = query_params.get('status')
if status_filter:
items = [item for item in items if item.get('status') == status_filter]
return build_response(200, items)
def get_due_soon(query_params):
student_id = query_params.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id query parameter'})
# Calculate date range: today through 7 days from now
today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
week_from_now = (datetime.now(timezone.utc) + timedelta(days=7)).strftime('%Y-%m-%d')
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('student_id').eq(student_id)
)
# Filter to incomplete assignments due within the next 7 days
items = response.get('Items', [])
due_soon_items = [
item for item in items
if item.get('status') == 'incomplete' and today <= item.get('due_date', '') <= week_from_now
]
return build_response(200, due_soon_items)
def update_assignment(assignment_id, event):
body = json.loads(event.get('body', '{}'))
student_id = body.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id in body'})
# Dynamically build the update expression from provided fields
update_expression_parts = []
expression_values = {}
if 'status' in body:
update_expression_parts.append('status = :s')
expression_values[':s'] = body['status']
if 'priority' in body:
update_expression_parts.append('priority = :p')
expression_values[':p'] = body['priority']
if 'due_date' in body:
update_expression_parts.append('due_date = :d')
expression_values[':d'] = body['due_date']
if not update_expression_parts:
return build_response(400, {'message': 'No fields to update'})
response = table.update_item(
Key={'student_id': student_id, 'assignment_id': assignment_id},
UpdateExpression='SET ' + ', '.join(update_expression_parts),
ExpressionAttributeValues=expression_values,
ReturnValues='ALL_NEW'
)
return build_response(200, response.get('Attributes', {}))
def delete_assignment(assignment_id, query_params):
student_id = query_params.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id query parameter'})
table.delete_item(
Key={'student_id': student_id, 'assignment_id': assignment_id}
)
return build_response(200, {'message': 'Assignment deleted'})
def build_response(status_code, body):
# Format the response for API Gateway Lambda proxy integration
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(body, default=str)
}
What does this code do?
The lambda_handler function is the entry point. It reads the HTTP method and path from the incoming event, then routes to the correct handler function.
- create_assignment generates a unique ID, sets defaults (status = incomplete, priority = medium), and writes the item to DynamoDB.
- get_assignments queries by student_id (primary key) or course_code (using your GSI). It also supports filtering by completion status.
- get_due_soon fetches all assignments for a student, then filters to only incomplete ones due within 7 days.
- update_assignment dynamically builds an UpdateExpression from whichever fields the request includes.
- delete_assignment removes an item using both the partition key and sort key.
- build_response formats every response with the statusCode, headers, and body structure that API Gateway expects.
- Click Deploy to save and deploy your function code.
You should see a green banner that says the function was updated successfully.
Seeing a deployment error?
Check that you deleted all the original default code before pasting. Any leftover lines from the default handler can cause syntax errors.
Also confirm the indentation looks correct. Python requires consistent spacing. If you see an indentation error, try selecting all and re-pasting.
help me fix a Lambda deployment error in my Python function
✔️ Awesome, I've got everything!
Great. Make sure you clicked Deploy and saw the green success banner before continuing.
ⓧ I'd like to double check the full code
Here is the complete lambda_function.py for comparison:
import json
import boto3
import uuid
from datetime import datetime, timedelta, timezone
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('StudentAssignments')
def lambda_handler(event, context):
http_method = event.get('httpMethod', '')
path = event.get('path', '/')
query_params = event.get('queryStringParameters') or {}
try:
if path == '/assignments' and http_method == 'POST':
return create_assignment(event)
elif path == '/assignments' and http_method == 'GET':
return get_assignments(query_params)
elif path.startswith('/assignments/') and http_method == 'PATCH':
assignment_id = path.split('/')[-1]
return update_assignment(assignment_id, event)
elif path.startswith('/assignments/') and http_method == 'DELETE':
assignment_id = path.split('/')[-1]
return delete_assignment(assignment_id, query_params)
elif path == '/assignments/due-soon' and http_method == 'GET':
return get_due_soon(query_params)
else:
return build_response(404, {'message': 'Not found'})
except Exception as e:
return build_response(500, {'message': str(e)})
def create_assignment(event):
body = json.loads(event.get('body', '{}'))
item = {
'student_id': body['student_id'],
'assignment_id': str(uuid.uuid4()),
'course_code': body['course_code'],
'title': body['title'],
'due_date': body['due_date'],
'priority': body.get('priority', 'medium'),
'status': 'incomplete',
'created_at': datetime.now(timezone.utc).isoformat()
}
table.put_item(Item=item)
return build_response(201, item)
def get_assignments(query_params):
student_id = query_params.get('student_id')
course_code = query_params.get('course_code')
if course_code:
response = table.query(
IndexName='CourseIndex',
KeyConditionExpression=boto3.dynamodb.conditions.Key('course_code').eq(course_code)
)
elif student_id:
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('student_id').eq(student_id)
)
else:
return build_response(400, {'message': 'Provide student_id or course_code query parameter'})
items = response.get('Items', [])
status_filter = query_params.get('status')
if status_filter:
items = [item for item in items if item.get('status') == status_filter]
return build_response(200, items)
def get_due_soon(query_params):
student_id = query_params.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id query parameter'})
today = datetime.now(timezone.utc).strftime('%Y-%m-%d')
week_from_now = (datetime.now(timezone.utc) + timedelta(days=7)).strftime('%Y-%m-%d')
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('student_id').eq(student_id)
)
items = response.get('Items', [])
due_soon_items = [
item for item in items
if item.get('status') == 'incomplete' and today <= item.get('due_date', '') <= week_from_now
]
return build_response(200, due_soon_items)
def update_assignment(assignment_id, event):
body = json.loads(event.get('body', '{}'))
student_id = body.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id in body'})
update_expression_parts = []
expression_values = {}
if 'status' in body:
update_expression_parts.append('status = :s')
expression_values[':s'] = body['status']
if 'priority' in body:
update_expression_parts.append('priority = :p')
expression_values[':p'] = body['priority']
if 'due_date' in body:
update_expression_parts.append('due_date = :d')
expression_values[':d'] = body['due_date']
if not update_expression_parts:
return build_response(400, {'message': 'No fields to update'})
response = table.update_item(
Key={'student_id': student_id, 'assignment_id': assignment_id},
UpdateExpression='SET ' + ', '.join(update_expression_parts),
ExpressionAttributeValues=expression_values,
ReturnValues='ALL_NEW'
)
return build_response(200, response.get('Attributes', {}))
def delete_assignment(assignment_id, query_params):
student_id = query_params.get('student_id')
if not student_id:
return build_response(400, {'message': 'Provide student_id query parameter'})
table.delete_item(
Key={'student_id': student_id, 'assignment_id': assignment_id}
)
return build_response(200, {'message': 'Assignment deleted'})
def build_response(status_code, body):
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(body, default=str)
}
Test the function with sample events
Lambda lets you invoke your function with test events directly in the console. You'll simulate an API Gateway request to verify everything works end-to-end with DynamoDB.
- Click the Test tab in the Lambda console.
- Under Test event, choose Create new event.
- Enter CreateAssignment as the event name.
- Replace the default JSON with the following test payload:
{
"httpMethod": "POST",
"path": "/assignments",
"queryStringParameters": null,
"body": "{\"student_id\": \"student-001\", \"course_code\": \"CS601\", \"title\": \"Build REST API\", \"due_date\": \"2026-06-15\", \"priority\": \"high\"}"
}
What does this test event simulate?
This JSON mimics the structure API Gateway sends to Lambda when using proxy integration. The httpMethod and path fields tell your handler which route to use. The body field contains the assignment data as a stringified JSON object.
- Click Save changes, then click Test.
Under Execution result, expand Details. You should see a response with statusCode of 201 and a body containing your assignment item with a generated assignment_id.
Now test the GET route to confirm you can read assignments back from DynamoDB.
- In the Test event dropdown at the top of the Test tab, choose Create new event.
- Enter GetAssignments as the event name.
- Replace the JSON with:
{
"httpMethod": "GET",
"path": "/assignments",
"queryStringParameters": {
"student_id": "student-001"
},
"body": null
}
- Click Save changes, then click Test.
You should see a statusCode of 200 and a body containing the assignment you created in the previous test.
- To double-check the data landed in DynamoDB, open the DynamoDB console in a new tab and click Explore table items on your StudentAssignments table.
- Confirm you see the new item with student_id of student-001 and the title Build REST API.
Test returning a 500 error?
If you see a message like An error occurred (AccessDeniedException), the DynamoDB policy was not attached correctly. Go back to IAM and confirm AmazonDynamoDBFullAccess appears in the role's permissions list.
If you see a KeyError, check that your test event JSON includes all required fields: student_id, course_code, title, and due_date.
help me debug my Lambda function test failure
Your Lambda function is deployed and verified. Next up, you'll connect it to API Gateway so it can receive real HTTP requests from the internet.
Create and Configure API Gateway
Your Lambda function is deployed and tested. Now you need a way for the outside world to reach it. Right now, the only way to invoke your function is through the Lambda console's test feature.
In this step, you'll create an API Gateway REST API that acts as the front door to your Lambda function. API Gateway gives you a public URL that accepts HTTP requests and forwards them to your function. This is the same pattern used in production serverless applications.
In this step, get ready to:
- Create a REST API in API Gateway with a Regional endpoint.
- Set up a proxy resource and Lambda proxy integration to route all requests to your function.
- Deploy the API and test it with curl commands.
Create the REST API
API Gateway offers multiple API types. A REST API provides the most feature-rich option and is the standard for AWS certification exams and enterprise applications.
- Type API Gateway into the AWS console search bar and select API Gateway.
- Click Create API.
- Find the REST API box (not "REST API Private") and click Build.
- Under API details, leave New API selected.
- For API name, enter AssignmentTrackerAPI.
- Keep API endpoint type set to Regional.
- Click Create API.
Why Regional endpoint type?
A Regional endpoint deploys the API in your chosen region (us-east-1). This keeps latency low for testing and costs nothing extra. Edge-optimized endpoints use CloudFront, which is overkill for a learning project.
Create the proxy resource and methods
A proxy resource with a greedy path variable catches all requests regardless of their URL path. Combined with an ANY method, this lets your single Lambda function handle all routing logic internally.
- On the Resources page, click Create resource.
- Turn on Proxy resource.
- Keep Resource path as /.
- For Resource name, enter {proxy+}.
- Click Create resource.
You should see a /{proxy+} resource appear in the resource tree with an ANY method beneath it. Now configure that method to point to your Lambda function.
- Select the /{proxy+} resource, then click Create method.
- For Method type, select ANY.
- For Integration type, select Lambda function.
- Turn on Lambda proxy integration.
- For Lambda function, type AssignmentTrackerFunction and select it from the dropdown.
- Click Create method.
What is Lambda proxy integration?
Lambda proxy integration passes the entire HTTP request (method, path, headers, query strings, body) directly to your Lambda function as the event parameter. Your function handles routing internally and returns a response object that API Gateway converts back to an HTTP response.
This is simpler than custom integration because you don't need to write mapping templates. It is also the most common pattern in production serverless applications.
The proxy resource only catches requests to subpaths like /assignments. Requests to the root path / need their own method. Set that up now.
- In the resource tree, select the / (root) resource.
- Click Create method.
- For Method type, select ANY.
- For Integration type, select Lambda function.
- Turn on Lambda proxy integration.
- For Lambda function, type AssignmentTrackerFunction and select it from the dropdown.
- Click Create method.
Why add a method on the root resource too?
The greedy path variable {proxy+} only matches paths with at least one segment after the slash (like /assignments). A request to the bare root / would return a 403 error without its own method. Adding ANY on the root ensures complete coverage.
Now deploy the API so it gets a live URL. An API Gateway API is not accessible until you deploy it to a stage.
- On the Resources page, click Deploy API.
- For Stage, select New stage.
- For Stage name, enter prod.
- Click Deploy.
- Copy the Invoke URL shown in the Stage details section. Save it somewhere accessible. You will use it for all your API testing.
Save your invoke URL
The invoke URL looks like https://abc123def.execute-api.us-east-1.amazonaws.com/prod. Paste it into a text file or sticky note. You will use it repeatedly in the next substep.
Don't see a Deploy API button?
Make sure you are on the Resources page (not the Stages page). The Deploy API button appears in the top-right area of the Resources page.
If you see a permission prompt asking to add a resource-based policy to your Lambda function, click OK. This grants API Gateway permission to invoke your function.
Help me deploy my API Gateway REST API.
Test your API with curl
Your API is live. Time to confirm it works by sending real HTTP requests using curl from AWS CloudShell. CloudShell is a browser-based terminal built into the AWS console with curl pre-installed.
- Click the CloudShell icon on the console navigation bar (to the right of the search box).
- Wait for the shell to initialize. When you see a command prompt, it is ready.
First, create an assignment using a POST request.
- Create your first assignment by running this command (replace the URL with your invoke URL):
curl -X POST [[INVOKE_URL="your API invoke URL"]]/assignments \
-H "Content-Type: application/json" \
-d '{
"student_id": "student1",
"course_code": "CS601",
"title": "Build REST API",
"due_date": "2026-06-01",
"priority": "high"
}'
What does this command do?
This sends a POST request to your /assignments endpoint with a JSON body containing assignment details. Your Lambda function receives this, generates a unique assignment_id, stores the item in DynamoDB, and returns the created item as JSON.
You should see a JSON response with a 201 status and the full assignment object including the generated assignment_id. Copy that assignment_id value. You will need it for the PATCH test.
Next, retrieve your assignments with a GET request.
- List all assignments for student1 by running:
curl [[INVOKE_URL="your API invoke URL"]]/assignments?student_id=student1
You should see a JSON array containing the assignment you just created.
Now test updating an assignment's status using PATCH.
- Mark the assignment as complete by running this command (replace the assignment ID with the one from your POST response):
curl -X PATCH [[INVOKE_URL="your API invoke URL"]]/assignments/[[ASSIGNMENT_ID="your assignment_id from the POST response"]] \
-H "Content-Type: application/json" \
-d '{
"student_id": "student1",
"status": "complete"
}'
You should see the updated assignment with status changed to complete.
Finally, test querying by course code using the Global Secondary Index.
- Get all assignments for course CS601 by running:
curl [[INVOKE_URL="your API invoke URL"]]/assignments?course_code=CS601
You should see the same assignment returned, this time queried through your CourseIndex GSI.
Getting a 500 Internal Server Error?
Check the following common causes:
- Make sure your Lambda function's execution role has the AmazonDynamoDBFullAccess policy attached. Without it, the function cannot read or write to DynamoDB.
- Verify the table name in your Lambda code is exactly StudentAssignments (case-sensitive).
- Check that your invoke URL ends with /prod (the stage name), not a trailing slash after that.
Help me debug my API Gateway 500 error.
Your serverless API is live and accepting requests from the internet. In the next step, you will put it through more rigorous testing with multiple assignments across different courses.
Test Your API End-to-End
In the previous step, you wired Lambda, DynamoDB, and API Gateway together and ran a quick test to confirm the connection works.
Now you'll exercise every endpoint in your API with realistic data. You'll create assignments across multiple courses, query them with different filters, and confirm that updates and deletes work as expected.
In this step, get ready to:
- Create multiple assignments across different courses using POST requests.
- Test filtering endpoints including course queries, due-soon, and status filters.
- Update and delete assignments to confirm write operations work.
Create multiple test assignments
A real API needs realistic data to test properly. You'll create assignments across three different courses with varying due dates and priorities so you can exercise every query path your Lambda function supports.
Adjust dates to today
The due-soon endpoint returns assignments due within 7 days of today. Set at least two of your due dates within the next 7 days so you can verify that endpoint later.
Windows users: curl syntax
The commands below use bash-style single quotes (macOS and Linux). On Windows, open Git Bash (installed with Git) for identical syntax. If you use PowerShell instead, replace single quotes with double quotes and escape inner quotes with a backslash.
- Open a terminal on your computer.
- Create your first assignment (a high-priority CS601 project due soon) by running this command:
curl -X POST [[INVOKE_URL="your prod stage invoke URL"]]/assignments \
-H "Content-Type: application/json" \
-d '{"student_id": "student1", "course_code": "CS601", "title": "Database Design Project", "due_date": "2026-05-25", "priority": "high"}'
What does this command do?
This sends a POST request to your /assignments endpoint with a JSON body containing the assignment details.
Your Lambda function generates a unique assignment_id, sets the status to incomplete, and stores the item in DynamoDB.
You should see a JSON response with a 201 status and all the assignment fields including the generated assignment_id.
- Copy the assignment_id value from the response and save it somewhere. You'll need it later for update and delete operations.
- Create a second assignment (a medium-priority CS602 assignment also due soon) by running:
curl -X POST [[INVOKE_URL="your prod stage invoke URL"]]/assignments \
-H "Content-Type: application/json" \
-d '{"student_id": "student1", "course_code": "CS602", "title": "Algorithm Analysis Homework", "due_date": "2026-05-23", "priority": "medium"}'
- Create a third assignment (a low-priority MATH501 assignment due further in the future) by running:
curl -X POST [[INVOKE_URL="your prod stage invoke URL"]]/assignments \
-H "Content-Type: application/json" \
-d '{"student_id": "student1", "course_code": "MATH501", "title": "Linear Algebra Problem Set", "due_date": "2026-06-15", "priority": "low"}'
- Create one more CS601 assignment with a later due date to give that course multiple entries:
curl -X POST [[INVOKE_URL="your prod stage invoke URL"]]/assignments \
-H "Content-Type: application/json" \
-d '{"student_id": "student1", "course_code": "CS601", "title": "SQL Query Optimization Lab", "due_date": "2026-06-10", "priority": "medium"}'
Each response should return a 201 status with the full item including the generated assignment_id and created_at timestamp.
Getting a 500 error or no response?
Double-check that your invoke URL ends with /prod (the stage name) and you're appending /assignments after it. The full path should look like https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/assignments.
If you see {"message": "Internal server error"}, return to your Lambda function and check the CloudWatch logs for a detailed error message.
Help me troubleshoot this POST error
Test the filtering endpoints
Your API supports three different query patterns. Each one demonstrates a different DynamoDB access strategy, from the Global Secondary Index to in-memory filtering.
- Query all assignments for course CS601 (this uses your CourseIndex GSI) by running:
curl "[[INVOKE_URL="your prod stage invoke URL"]]/assignments?course_code=CS601"
Why does this use the GSI?
Your table's primary key is student_id, so you can't efficiently query by course on the base table. The CourseIndex Global Secondary Index re-organizes the data with course_code as the partition key, making this query fast regardless of how many students exist.
You should see both CS601 assignments (Database Design Project and SQL Query Optimization Lab) in the response array, sorted by due_date.
- Query assignments due within the next 7 days by running:
curl "[[INVOKE_URL="your prod stage invoke URL"]]/assignments/due-soon?student_id=student1"
You should see only the assignments with due dates within 7 days of today. The MATH501 assignment (due 2026-06-15) and the later CS601 assignment should not appear.
- Query all assignments for student1 filtered by status by running:
curl "[[INVOKE_URL="your prod stage invoke URL"]]/assignments?student_id=student1&status=incomplete"
Since you haven't marked any assignments complete yet, all four assignments should appear.
Getting an empty array or error?
If the course query returns nothing, confirm you typed CS601 exactly as it appears in your POST requests (case-sensitive). If the due-soon query returns nothing, verify that at least one of your due dates is within 7 days of today.
Help me debug my GET query parameters
Test update and delete operations
The final piece of CRUD is confirming that you can modify and remove assignments. You'll mark one assignment as complete with PATCH, then permanently delete another with DELETE.
- Mark the CS602 assignment as complete by running this PATCH command (replace the assignment ID with the one you saved earlier from your CS602 POST response):
curl -X PATCH [[INVOKE_URL="your prod stage invoke URL"]]/assignments/[[ASSIGNMENT_ID="assignment_id from your CS602 POST response"]] \
-H "Content-Type: application/json" \
-d '{"student_id": "student1", "status": "complete"}'
What does this response show?
The PATCH response returns the full updated item (thanks to ReturnValues='ALL_NEW' in the Lambda code). You should see "status": "complete" in the response alongside all other fields unchanged.
- Verify the status filter now works differently by running:
curl "[[INVOKE_URL="your prod stage invoke URL"]]/assignments?student_id=student1&status=complete"
You should see only the CS602 assignment in the response, confirming that the status filter correctly narrows results.
- Delete the MATH501 assignment by running this DELETE command (replace the assignment ID with the one from your MATH501 POST response):
curl -X DELETE "[[INVOKE_URL="your prod stage invoke URL"]]/assignments/[[ASSIGNMENT_ID="assignment_id from your MATH501 POST response"]]?student_id=student1"
You should see {"message": "Assignment deleted"} in the response.
- Verify the deletion in the DynamoDB console. Switch back to the DynamoDB service in the AWS console.
- Click **Tables**, then click **StudentAssignments**.
- Click **Explore table items**.
- Confirm that the MATH501 assignment no longer appears and the CS602 assignment shows status as complete.
Assignment still showing after delete?
DynamoDB deletes apply immediately, but the console viewer may cache results. Click the refresh icon in the table explorer. Also confirm you used the correct assignment_id and student_id in your DELETE command. Both must match exactly.
Help me troubleshoot my DELETE operation
Your serverless API is fully tested and working. You've proven that every endpoint handles real data correctly, from creation through querying, updating, and deletion. This is a complete, portfolio-ready project you can demo in interviews.
Secret mission
Add Priority Filtering and Stats Endpoint
Your API handles the basics. But what if you want to quickly find only your high-priority assignments, or see how many assignments you've completed per course? Add a priority filter and a stats endpoint to make your tracker a real productivity tool.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Lambda and DynamoDB are always-free tier services, and API Gateway only charges when invoked. At learning-project scale, ongoing costs are negligible.
Resources you used:
- DynamoDB table StudentAssignments (always-free: 25 GB storage, 25 read/write capacity units).
- Lambda function AssignmentTrackerFunction (always-free: 1 million requests per month).
- API Gateway REST API AssignmentTrackerAPI deployed to prod stage (uses Free Plan credits when invoked).
- IAM execution role for the Lambda function (with AmazonDynamoDBFullAccess policy attached).
Keep everything running
No action needed. Choose this if you plan to keep building on this API or want to demo it in interviews.
- Lambda and DynamoDB are always-free services. They will not consume your Free Plan credits at this scale.
- API Gateway only incurs charges when requests are made. If nobody calls the endpoint, there is no cost.
- Your invoke URL remains live. You can share it or use it in a portfolio project.
Pause - I'll come back to this later
Serverless services only charge when used. There is nothing to pause because Lambda, DynamoDB, and API Gateway are all idle by default.
- Your resources are already in a paused state. No running servers or containers exist.
- If you want to prevent accidental usage, you can delete the API Gateway deployment stage. This removes the public URL but keeps your API configuration intact for redeployment later.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps in order to avoid dependency conflicts.
- Delete the API Gateway REST API first. Search for API Gateway in the AWS console search bar. Select AssignmentTrackerAPI from the APIs list. Click Delete and confirm.
- Delete the Lambda function. Search for Lambda in the console search bar. Select AssignmentTrackerFunction from the Functions list. Click Actions, then Delete. Type delete to confirm.
- Delete the DynamoDB table. Search for DynamoDB in the console search bar. Select Tables in the sidebar. Select StudentAssignments and click Delete. Confirm by typing delete in the confirmation box.
- Delete the IAM execution role. Search for IAM in the console search bar. Select Roles in the sidebar. Search for the role name that starts with AssignmentTrackerFunction-role. Select the role and click Delete.
Nice Work!
Nice Work!
You just built a production-grade serverless API on AWS from scratch. You now have a live endpoint that handles CRUD operations, date-based queries, and filtering across multiple access patterns.
You've learned how to:
- Designed a DynamoDB table with composite keys and a Global Secondary Index to support multiple access patterns like querying by course and filtering by due date.
- Wrote a Lambda function in Python that handles routing, CRUD operations, and business logic including date comparisons for the "due soon" feature.
- Connected everything through API Gateway with Lambda proxy integration and deployed a live API endpoint you can invoke from anywhere in the world.
- Secret Mission: Added priority filtering and a stats endpoint that returns assignment completion percentages per course.
Ready to quiz yourself?