DEV Community

Cover image for Building a Serverless Notes App with AWS Amplify, Cognito, Lambda, DynamoDB & API Gateway
Siddhesh Mulik
Siddhesh Mulik

Posted on

Building a Serverless Notes App with AWS Amplify, Cognito, Lambda, DynamoDB & API Gateway

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:

  1. User accesses the website → Hosted on AWS Amplify
  2. Amplify redirects to Cognito → For user authentication
  3. Cognito authenticates → User gains access to the app
  4. User performs actions → Amplify sends requests to API Gateway
  5. API Gateway triggers Lambda → Processes the business logic
  6. Lambda interacts with DynamoDB → Stores/retrieves notes data

Architectural diagram

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.

  1. Navigate to DynamoDB in AWS Console
  2. Click Create table
  3. Configure the table:
    • Table name: Notes
    • Partition key: notesId (String)
  4. Keep other settings as default
  5. Click Create table

Create DynamoDB 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.

  1. Go to AWS Lambda console
  2. Click Create function
  3. Configure:
    • Function name: notes-lambda
    • Runtime: Python 3.x
  4. Click Create function

Create Lambda Function


Step 3: Configure Lambda Permissions

Lambda needs permission to interact with DynamoDB. Let's set that up.

3.1 Access IAM Role

  1. In your Lambda function, go to Configuration tab
  2. Click Permissions in the left sidebar
  3. Click on the Role name (opens IAM console)

Access IAM Role

3.2 Create Inline Policy

  1. Under Permissions policies, click Add permissionsCreate inline policy

Create Inline Policy

  1. Configure the policy:
    • Service: Select DynamoDB
    • Actions:
      • Read: Check Scan
      • Write: Check PutItem, DeleteItem

Configure the policy:

3.3 Specify Resources

  1. In Resources section, click Add ARN
  2. Go to your DynamoDB table and copy the Table ARN
  3. Paste the ARN in the policy
  4. Click Next

Specify Resources

3.4 Create Policy

  1. Policy name: Notes-lambda-policy
  2. Click Create policy

Create Policy

3.5 Add Lambda Code

  1. Go back to your Lambda function
  2. 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)
Enter fullscreen mode Exit fullscreen mode
  • Click Deploy

Deploy Lambda Function


Step 4: Create API Gateway

API Gateway will expose our Lambda function as REST endpoints.

4.1 Create REST API

  1. Go to API Gateway console
  2. Find REST API and click Build
  3. Configure:
    • API name: notesAPI
    • Endpoint Type: Regional
  4. Click Create API

Create REST API

4.2 Create /notes Resource

  1. Click Create resource
  2. Resource name: notes
  3. Click Create resource

Create /notes Resource

4.3 Create GET Method

  1. Select /notes resource
  2. Click Create method
  3. Configure:
    • Method type: GET
    • Integration type: Lambda function
    • Enable Lambda proxy integration: ✅ Check this
    • Lambda function: Select notes-lambda
  4. Click Create method

Create GET Method

4.4 Create POST Method

Repeat the same process for POST:

  1. Select /notes resource
  2. Create methodPOST
  3. Enable Lambda proxy integration
  4. Select notes-lambda
  5. Create method

4.5 Create /{notesId} Resource

For deleting individual notes:

  1. Select /notes resource
  2. Click Create resource
  3. Resource name: {notesId} (include the curly braces)
  4. Click Create resource

Create /{notesId} Resource

4.6 Create DELETE Method

  1. Select /{notesId} resource
  2. Create methodDELETE
  3. Enable Lambda proxy integration
  4. Select notes-lambda
  5. Create method

4.7 Enable CORS for /notes

  1. Select /notes resource
  2. Click Enable CORS
  3. In Access-Control-Allow-Methods:

    • Check GET
    • Check POST
  4. Click Save

Enable CORS for /notes

4.8 Enable CORS for /{notesId}

  1. Select /{notesId} resource
  2. Click Enable CORS
  3. In Access-Control-Allow-Methods:
    • Check DELETE
  4. Click Save

4.9 Deploy API

  1. Click Deploy API
  2. Configure:
    • Stage: *New Stage*
    • Stage name: dev
  3. Click Deploy
  4. Copy the Invoke URL — you'll need this later!

Deploy API

💡 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

  1. Go to Amazon Cognito console
  2. Click Create user pool
  3. 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

Create User Pool

5.2 Configure Sign-up

  1. In Required attributes, check:
    • email
  2. Keep other settings default
  3. Click Create user directory

Configure Sign-up


Step 6: Edit Password Policy

Let's make the password requirements simpler for this tutorial.

  1. Go to your User Pool
  2. Navigate to Authentication methods tab
  3. Scroll to Password policy → Click Edit
  4. Select Custom password policy:
    • Minimum length: 6 characters
    • Password requirements: Deselect all complexity requirements
    • Prevent reuse of previous: 2 passwords
  5. Click Save changes

Edit Password Policy

⚠️ 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

  1. Go to AWS Amplify console
  2. Click Deploy an app

Connect Repository

  1. Select GitHubNext

Select GitHub

  1. Authorize Amplify to access your GitHub
  2. Select your repository and branch
  3. Click Next

Authorize ampligy to access your GitHub

7.2 Configure Build Settings

  1. App name: Keep default or customize
  2. In Build settings:
    • Build command: npm run build
    • Build output directory: dist

Configure Build Settings

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)

Add Environment Variables

Where to find Cognito values:

  • Go to your Cognito User Pool

  • Cognito Authority: Overview tab

  • App client ID: App integration tab → App clients

  • Click NextSave 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.

  1. Copy your Amplify domain URL (from Amplify console)
  2. Go back to Cognito → Your User Pool
  3. Click on your App client
  4. Scroll to Login Pages → Click Edit
  5. In Allowed callback URLs, paste your Amplify URL
    • ⚠️ Important: Remove any trailing slashes!
    • Example: [https://main.d1234abcd.amplifyapp.com]
  6. Click Save changes

Add Redirect URL to Cognito


Step 9: Test Your Application! 🎉

Your serverless notes app is now live! Let's test it.

9.1 Access the App

  1. Paste your Amplify domain URL in a browser
  2. You should see your app's sign-in page

Access the App

9.2 Create an Account

  1. Click Create an account

Create an Account

  1. Fill in the registration form:
    • Username
    • Email address
    • Password
    • Confirm password
  2. Click Sign up

Fill in the registration form

9.3 Verify Email

  1. Check your email inbox
  2. Copy the verification code
  3. Enter it in the confirmation page
  4. Click Confirm

Verify Email

9.4 Test Note Operations

  1. Create a note: Add title and content → Click Add note

Create a note

  1. View notes: All notes should display
  2. Delete a note: Click delete button on any note

Delete a 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 notesId as a string: "123" instead of 123

  • Or 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:


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)

Collapse
 
siddhesmm303 profile image
Siddhesh Mulik

Thanks for reading! If you have any suggestions or improvements, I'm eager to hear from you. Drop a comment below! 👇