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)
-
Table name:
- 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-lambda -
Runtime:
Python 3.x
-
Function name:
- 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
DynamoDB -
Actions:
- Read: Check
Scan - Write: Check
PutItem,DeleteItem
- Read: Check
-
Service: Select
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-policy - Click 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
-
API name:
- Click Create API
4.2 Create /notes Resource
- Click Create resource
-
Resource name:
notes - Click Create resource
4.3 Create GET Method
- Select
/notesresource - Click Create method
- Configure:
-
Method type:
GET - Integration type: Lambda function
- Enable Lambda proxy integration: ✅ Check this
-
Lambda function: Select
notes-lambda
-
Method type:
- Click Create method
4.4 Create POST Method
Repeat the same process for POST:
- Select
/notesresource -
Create method →
POST - Enable Lambda proxy integration
- Select
notes-lambda - Create method
4.5 Create /{notesId} Resource
For deleting individual notes:
- Select
/notesresource - Click Create resource
-
Resource name:
{notesId}(include the curly braces) - Click Create resource
4.6 Create DELETE Method
- Select
/{notesId}resource -
Create method →
DELETE - Enable Lambda proxy integration
- Select
notes-lambda - Create method
4.7 Enable CORS for /notes
- Select
/notesresource - Click Enable CORS
-
In Access-Control-Allow-Methods:
- Check
GET - Check
POST
- Check
Click Save
4.8 Enable CORS for /{notesId}
- Select
/{notesId}resource - Click Enable CORS
- In Access-Control-Allow-Methods:
- Check
DELETE
- Check
- Click Save
4.9 Deploy API
- Click Deploy API
- Configure:
-
Stage:
*New Stage* -
Stage name:
dev
-
Stage:
- 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
-
Application type:
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
-
Minimum length:
- 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
-
Build command:
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]
- 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
- Click Create an account
- Fill in the registration form:
- 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 (1)
Thanks for reading! If you have any suggestions or improvements, I'm eager to hear from you. Drop a comment below! 👇