Introduction
In this tutorial, you'll learn how to build a fully serverless notes application using AWS services. By the end, you'll have a production-ready app with user authentication, a REST API, and a NoSQL database—all without managing a single server.
What we're building: A notes app where users can create, view, and delete notes with secure authentication.
Tech Stack:
AWS Amplify - Frontend hosting
Amazon Cognito - User authentication
API Gateway - REST API endpoints
AWS Lambda - Serverless functions
DynamoDB - NoSQL database
Prerequisites:
AWS account
GitHub account
Basic knowledge of React/JavaScript
Basic understanding of REST APIs
Architecture Overview
Before we dive into implementation, let's understand how these services work together:
User accesses the website → Hosted on AWS Amplify
Amplify redirects to Cognito → For user authentication
Cognito authenticates → User gains access to the app
User performs actions → Amplify sends requests to API Gateway
API Gateway triggers Lambda → Processes the business logic
Lambda interacts with DynamoDB → Stores/retrieves notes data
Now let's build this step by step!
Step 1: Create DynamoDB Table
DynamoDB will store all our notes data. Let's set it up first.
Navigate to DynamoDB in AWS Console
Click Create table
Configure the table:
Table name: Notes
Partition key: notesId (String)
Keep other settings as default
Click Create table
💡 Why this matters: The partition key (notesId) uniquely identifies each note. Make sure it's set as String type—this will save you from type mismatch errors later!
Step 2: Create Lambda Function
Lambda will handle all our backend logic—creating, reading, and deleting notes.
Go to AWS Lambda console
Click Create function
-
Configure:
Function name:
notes-lambdaRuntime:
Python 3.x Click Create function
Step 3: Configure Lambda Permissions
Lambda needs permission to interact with DynamoDB. Let's set that up.
3.1 Access IAM Role
In your Lambda function, go to Configuration tab
Click Permissions in the left sidebar
Click on the Role name (opens IAM console)
3.2 Create Inline Policy
-
Under Permissions policies, click Add permissions → Create inline policy
-
Configure the policy:
Service: Select
DynamoDBActions:
-
Read: Check
Scan- Write: Check
PutItem,DeleteItem
- Write: Check
-
3.3 Specify Resources
In Resources section, click Add ARN
Go to your DynamoDB table and copy the Table ARN
Paste the ARN in the policy
Click Next
3.4 Create Policy
Policy name:
Notes-lambda-policyClick Create policy
3.5 Add Lambda Code
Go back to your Lambda function
In the Code tab, add your function code:
import json
import boto3
from botocore.exceptions import BotoCoreError, ClientError
def _response(status_code: int, body: dict, headers: dict):
"""Helper to build API Gateway compatible responses."""
return {
"statusCode": status_code,
"headers": headers,
"body": json.dumps(body),
}
def lambda_handler(event, context):
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Notes")
method = event.get("httpMethod", "")
# Common CORS headers for all responses
headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET,POST,DELETE,OPTIONS",
}
# Handle CORS preflight
if method == "OPTIONS":
return _response(200, {"message": "CORS preflight OK"}, headers)
try:
if method == "GET":
# Fetch all notes
response = table.scan()
items = response.get("Items", [])
return _response(200, items, headers)
elif method == "POST":
# Safely parse body
raw_body = event.get("body") or "{}"
try:
body = json.loads(raw_body)
except json.JSONDecodeError:
return _response(400, {"error": "Invalid JSON body"}, headers)
notes_id = body.get("notesId")
title = body.get("title")
desc = body.get("desc")
# Basic validation
if not notes_id or not title or not desc:
return _response(
400,
{"error": "notesId, title and desc are required fields"},
headers,
)
table.put_item(
Item={
"notesId": str(notes_id),
"title": title,
"desc": desc,
}
)
return _response(
201,
{"message": "Note created successfully", "notesId": str(notes_id)},
headers,
)
elif method == "DELETE":
path_params = event.get("pathParameters") or {}
notes_id = path_params.get("notesId")
if not notes_id:
return _response(400, {"error": "notesId is required in path"}, headers)
table.delete_item(Key={"notesId": str(notes_id)})
return _response(
200,
{"message": f"Note {notes_id} deleted successfully"},
headers,
)
# Method not allowed
return _response(
405, {"error": f"Method {method} not allowed for this resource"}, headers
)
except (BotoCoreError, ClientError) as e:
# Handle AWS/DynamoDB specific errors
return _response(500, {"error": "Database error", "detail": str(e)}, headers)
except Exception as e: # Catch-all for unexpected errors
return _response(500, {"error": "Internal server error", "detail": str(e)}, headers)
- Click Deploy
Step 4: Create API Gateway
API Gateway will expose our Lambda function as REST endpoints.
4.1 Create REST API
Go to API Gateway console
Find REST API and click Build
Configure:
* **API name**: `notesAPI`
* **Endpoint Type**: Regional
- Click Create API
4.2 Create /notes Resource
Click Create resource
Resource name:
notesClick Create resource
4.3 Create GET Method
Select
/notesresourceClick Create method
Configure:
* **Method type**: `GET`
* **Integration type**: Lambda function
* **Enable Lambda proxy integration**: ✅ Check this
* **Lambda function**: Select `notes-lambda`
- Click Create method
4.4 Create POST Method
Repeat the same process for POST:
Select
/notesresourceCreate method →
POSTEnable Lambda proxy integration
Select
notes-lambdaCreate method
4.5 Create /{notesId} Resource
For deleting individual notes:
Select
/notesresourceClick Create resource
Resource name:
{notesId}(include the curly braces)Click Create resource
4.6 Create DELETE Method
Select
/{notesId}resourceCreate method →
DELETEEnable Lambda proxy integration
Select
notes-lambdaCreate method
4.7 Enable CORS for /notes
Select
/notesresourceClick Enable CORS
In Access-Control-Allow-Methods:
* Check `GET`
* Check `POST`
- Click Save
4.8 Enable CORS for /{notesId}
Select
/{notesId}resourceClick Enable CORS
In Access-Control-Allow-Methods:
* Check `DELETE`
- Click Save
4.9 Deploy API
Click Deploy API
Configure:
* **Stage**: `*New Stage*`
* **Stage name**: `dev`
Click Deploy
Copy the Invoke URL — you'll need this later!
💡 Save this URL: Keep the invoke URL handy; you'll add it to your frontend environment variables.
Step 5: Create Cognito User Pool
Cognito will handle user registration and authentication.
5.1 Create User Pool
Go to Amazon Cognito console
Click Create user pool
Configure sign-in experience:
* **Application type**: `Single-page application (SPA)`
* **User name requirements**: My SPA app - notes
* **Cognito user pool sign-in options**: Check `Username`
5.2 Configure Sign-up
- In Required attributes, check:
* `email`
Keep other settings default
Click Create user directory
Step 6: Edit Password Policy
Let's make the password requirements simpler for this tutorial.
Go to your User Pool
Navigate to Authentication methods tab
Scroll to Password policy → Click Edit
Select Custom password policy:
* **Minimum length**: `6 characters`
* **Password requirements**: Deselect all complexity requirements
* **Prevent reuse of previous**: `2 passwords`
- Click Save changes
⚠️ Production Note: In production, use stronger password policies!
Step 7: Deploy Frontend on AWS Amplify
Time to deploy your React app!
7.1 Connect Repository
Go to AWS Amplify console
-
Click Deploy an app
-
Select GitHub → Next
Authorize Amplify to access your GitHub
Select your repository and branch
Click Next
7.2 Configure Build Settings
App name: Keep default or customize
In Build settings:
* **Build command**: `npm run build`
* **Build output directory**: `dist`
7.3 Add Environment Variables
Scroll to Advanced settings and add these environment variables:
| Variable Name | Value |
|---|---|
VITE_API_URL |
Your API Gateway invoke URL |
VITE_COGNITO_AUTHORITY |
From Cognito (Authority) |
VITE_COGNITO_CLIENT_ID |
From Cognito (App client ID) |
Where to find Cognito values:
Go to your Cognito User Pool
Cognito Authority: Overview tab
App client ID: App integration tab → App clients
- Click Next → Save and deploy
⏳ Wait time: Deployment usually takes 3-5 minutes.
Step 8: Add Redirect URL to Cognito
Now that Amplify has deployed your app, let's configure the callback URL.
Copy your Amplify domain URL (from Amplify console)
Go back to Cognito → Your User Pool
Click on your App client
Scroll to Login Pages → Click Edit
In Allowed callback URLs, paste your Amplify URL
* **⚠️ Important:** Remove any trailing slashes!
* Example: [`https://main.d1234abcd.amplifyapp.com`](https://main.d1234abcd.amplifyapp.com)
- Click Save changes
Step 9: Test Your Application! 🎉
Your serverless notes app is now live! Let's test it.
9.1 Access the App
Paste your Amplify domain URL in a browser
You should see your app's sign-in page
9.2 Create an Account
* Username
* Email address
* Password
* Confirm password
- Click Sign up
9.3 Verify Email
Check your email inbox
Copy the verification code
Enter it in the confirmation page
Click Confirm
9.4 Test Note Operations
-
Create a note: Add title and content → Click Add note
View notes: All notes should display
-
Delete a note: Click delete button on any note
🎊 Congratulations! Your serverless notes app is fully functional!
Common Errors & Solutions
While building this app, I encountered a few issues. Here's how to fix them:
Error 1: DynamoDB Type Mismatch
Problem: Lambda was failing when creating notes.
Cause: I was passing notesId as a number, but DynamoDB expected a string (as configured in the partition key).
Solution:
Always pass
notesIdas a string:"123"instead of123Or change your DynamoDB partition key type to Number (requires recreating the table)
Error 2: Amplify Build Failed
Problem: Amplify deployment failed with "package.json not found."
Cause: My frontend code was in a subdirectory, but Amplify was looking for package.json in the root.
Solution:
Move frontend code to repository root, OR
-
Update Amplify build settings:
- Go to App settings → Build settings → Edit
- Change Build command to:
cd frontend && npm run build - Change Base directory to:
frontend
Error 3: Cognito Authentication Error
Problem: "Requested page not found" error after clicking sign-in.
Cause: Wrong VITE_COGNITO_AUTHORITY in environment variables.
Solution:
Go to Cognito → App integration → Domain
Copy the exact domain (should look like:
https://your-domain.auth.region.amazoncognito.com)Update your Amplify environment variables
Redeploy the app
What's Next?
You've built a fully functional serverless notes app! Here are some ideas to extend it:
Features to Add:
✏️ Edit existing notes
🏷️ Add tags/categories
🔍 Search functionality
📤 Share notes with other users
🌙 Dark mode
Improvements:
Add input validation
Implement error boundaries in React
Add loading states
Use DynamoDB Global Secondary Indexes for better queries
Set up CloudWatch logs for monitoring
Cost Optimization:
Enable DynamoDB on-demand billing
Use Lambda reserved concurrency
Implement API caching in API Gateway
Conclusion
Building serverless applications with AWS is powerful and cost-effective. You've learned how to:
✅ Set up a DynamoDB table for data storage
✅ Create Lambda functions for backend logic
✅ Configure API Gateway for REST endpoints
✅ Implement authentication with Cognito
✅ Deploy a React app with Amplify
✅ Connect all services together
The best part? This entire infrastructure scales automatically and you only pay for what you use!
Resources:
Found this helpful? Drop a comment below with what you built or any questions you have!
Tags: #AWS #Serverless #Lambda #DynamoDB #Cognito #Amplify #Tutorial #WebDevelopment





























Top comments (0)