How to Build a Full-Stack Serverless CRUD App with AWS and React: Complete Guide
By Braincuber Team
Published on March 3, 2026
A D2C founder we consult for was paying $1,740/month for a Heroku-hosted inventory dashboard that served maybe 200 requests per day. Two dynos running 24/7. A managed Postgres add-on. Three Lambda-equivalent functions doing CRUD operations that took 300ms each. We rebuilt the entire thing on AWS serverless — DynamoDB, Lambda, API Gateway, Cognito, CloudFront. Monthly bill dropped to $8.30. Scales to zero when nobody's using it. Scales to thousands when they are. This complete tutorial walks you through building the same architecture, step by step.
What You'll Learn:
- Creating a DynamoDB table with partition keys and test items
- Setting up IAM roles with scoped DynamoDB and CloudWatch permissions
- Writing Lambda functions for GET, POST, PUT, DELETE operations
- Using Lambda Layers to share dependencies across functions
- Exposing Lambda functions via API Gateway with CORS configuration
- Adding Cognito authentication to secure your API endpoints
- Deploying a React frontend to S3 with CloudFront distribution
Why Serverless Kills Your Server Bill
"Serverless" doesn't mean there are no servers. It means AWS handles provisioning, managing, and scaling the infrastructure behind the scenes. You only pay for what you use. Lambda bills per invocation + duration + memory. DynamoDB bills per read/write unit. When traffic drops to zero at 3 AM, your bill drops to zero too.
Scale-to-Zero Billing
Lambda charges $0 when nobody's calling your API. Compare that to an EC2 instance or Heroku dyno burning $7-25/month running 24/7 doing nothing. For internal tools and dashboards with bursty traffic, serverless saves 80-95% on compute.
Auto-Scaling Built In
Lambda spins up new execution environments automatically. DynamoDB scales read/write capacity on demand. No load balancer tuning, no auto-scaling groups to configure. Your app handles 10 requests or 10,000 without any config changes.
Cognito Auth = No Password DB
AWS Cognito handles user sign-up, sign-in, MFA, and JWT token management. No storing passwords in your database. No bcrypt hashing. No session management. Cognito validates tokens with API Gateway before your Lambda code even runs.
Global CDN via CloudFront
Your React app gets served from 400+ CloudFront edge locations worldwide. Sub-100ms load times whether your customer is in New York, London, or Dubai. S3 stores the build files, CloudFront caches and delivers them.
Architecture Overview
DynamoDB stores the data. Lambda functions handle the API logic. API Gateway routes HTTP requests to the right Lambda. Cognito secures everything with JWT tokens. The React frontend connects to Cognito for auth and hits API Gateway for CRUD operations. S3 hosts the built React files, CloudFront delivers them globally.
React App (S3 + CloudFront) ↓ Cognito (JWT Auth) ↓ API Gateway (HTTP Routes) ↓ Lambda Functions (GET/POST/PUT/DELETE) ↓ DynamoDB (NoSQL Data Store)
The 10-Step Serverless Build
We're building a coffee shop inventory management system. Owners log in through Cognito, then create, read, update, and delete products. The same pattern works for any CRUD app — product catalogs, order trackers, customer databases.
Create a DynamoDB Table
Navigate to DynamoDB in the AWS console. Click Create table. Set the table name to "CoffeeShop" and the partition key to "coffeeId" (String). Click Create table. Then click Explore items > Create item and add a test record with attributes: coffeeId (String: "c123"), name (String: "Cold Brew"), price (Number: 456), available (Boolean: true).
Create an IAM Role for Lambda Functions
Navigate to IAM > Roles > Create role. Select Lambda as the service. Search for and attach AWSLambdaBasicExecutionRole. Name the role (e.g., "CoffeeShopRole") and create it. Then add an inline policy granting DynamoDB access: PutItem, DeleteItem, GetItem, Scan, UpdateItem — scoped to your CoffeeShop table ARN. This single role is shared across all Lambda functions.
Write and Deploy the GET Lambda Function
Create a folder structure: lambda/get/. Run npm init, set "type": "module" in package.json. Create index.mjs with a getCoffee function that uses DynamoDBClient and GetItemCommand from the AWS SDK v3. Install dependencies: npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb. Zip the folder with zip -r get.zip ./* and upload to AWS Lambda. Set the handler to index.getCoffee.
Create Lambda Layer for Shared Dependencies
All CRUD functions use the same AWS SDK. Instead of bundling it in every function, create a Lambda Layer. Create a folder called nodejs (exact name required). Run npm init inside it and install the SDK. Create a utils.mjs file that exports DynamoDBDocumentClient, all DynamoDB commands (Scan, Get, Put, Update, Delete), and a createResponse helper. Zip the nodejs folder as layer.zip and upload to Lambda Layers.
Refactor All Lambda Functions to Use the Layer
Update each function's index.mjs to import from the layer path: import { docClient, GetCommand, ScanCommand, createResponse } from '/opt/nodejs/utils.mjs'. Create separate folders for get, post, update, delete functions. Each function is now just the business logic — no node_modules. Zip each, upload to Lambda, and attach the DynamoDBLayer to each function.
Create API Gateway Routes for CRUD Endpoints
Navigate to API Gateway > Routes > Create. Create 5 routes: GET /coffee and GET /coffee/{id} pointing to getCoffee, POST /coffee to createCoffee, PUT /coffee/{id} to updateCoffee, DELETE /coffee/{id} to deleteCoffee. Go to Integrations, create Lambda integrations for each. Configure CORS: set Access-Control-Allow-Origin to your localhost URL, add content-type to Allow-Headers, and include GET, POST, OPTIONS, PUT, DELETE in Allow-Methods.
Set Up Cognito User Pool for Authentication
Navigate to Amazon Cognito > User pools > Create user pool. Select Single-page application (SPA). Choose email as the sign-in method. Set the return URL to your local dev URL (e.g., http://localhost:5174/). Click Create user directory. Then go to API Gateway > Authorization > Manage authorizers, create a JWT authorizer named "Cognito-CoffeeShop" using the issuer URL and Client ID from your Cognito user pool. Attach the authorizer to all API routes.
Integrate Cognito with React Frontend
In your React project, install npm install oidc-client-ts react-oidc-context --save. Update main.jsx to wrap your app with AuthProvider from react-oidc-context, configuring it with your Cognito authority URL, client_id, redirect_uri, and scopes (email openid phone). Update App.jsx to use the useAuth() hook for sign-in/sign-out flows. Run npm run dev and verify the login page loads with Cognito-hosted sign-up/sign-in.
Deploy React to S3 with CloudFront Distribution
Create an S3 bucket with a globally unique name. Run npm run build in the frontend folder to generate the dist folder. Upload the dist folder to S3. Create a CloudFront distribution: select your S3 bucket as origin, set origin path to /dist, select Origin access control. Set the Default root object to index.html. Update your React code and Cognito settings with the CloudFront distribution domain name.
Fix Access Denied and Verify Production Deployment
If you see an AccessDenied error, fix it in 3 steps: go to CloudFront > Origins, edit the S3 origin, select Origin access control settings, create a new OAC. Copy the generated S3 bucket policy and paste it in S3 > Permissions > Bucket policy. Set the Default root object to index.html in CloudFront General settings. Access the CloudFront URL in an incognito browser — your app should load with the Cognito login page.
The IAM Policy and Lambda Handler Code
Two files you'll reference constantly. The IAM policy scopes Lambda's DynamoDB access. The utility module lives in the Lambda Layer and gets shared across all functions.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb::YOUR_TABLE_ARN"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
ScanCommand, GetCommand,
PutCommand, UpdateCommand, DeleteCommand
} from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const createResponse = (statusCode, body) => ({
statusCode,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
export {
docClient, createResponse,
ScanCommand, GetCommand,
PutCommand, UpdateCommand, DeleteCommand
};
The CORS Trap That Wastes 3 Hours
When you set the localhost URL in API Gateway CORS settings, remove the trailing slash. Set it to http://localhost:5174 not http://localhost:5174/. That one trailing slash breaks CORS preflight requests. We've seen devs spend half a day debugging "why won't my React app connect to the API" when it's literally one character.
Lambda Layer Folder Name Must Be "nodejs"
The folder containing your shared dependencies must be named exactly "nodejs" — not "node", not "node_modules_shared", not "layer". AWS Lambda looks for /opt/nodejs/ at runtime. Name it anything else and your imports will fail with "Cannot find module '/opt/nodejs/utils.mjs'". We've seen this mistake in 4 out of 5 first-time Lambda Layer setups.
Frequently Asked Questions
How much does this serverless architecture cost per month?
For a low-traffic CRUD app handling 1,000-5,000 requests/day, expect $5-15/month total. DynamoDB on-demand costs ~$1.25/million writes and ~$0.25/million reads. Lambda's free tier covers 1M requests/month. CloudFront has a generous free tier too. The biggest cost is usually the custom domain SSL certificate if you add one.
Can I use this architecture for production applications?
Yes. This exact architecture handles production workloads. For higher traffic, add DynamoDB auto-scaling, enable Lambda Provisioned Concurrency to eliminate cold starts, and configure CloudFront caching policies. Add a custom domain through Route 53 and AWS Certificate Manager for professional URLs.
Why DynamoDB instead of RDS or Aurora?
DynamoDB is serverless and scales to zero. RDS and Aurora have minimum monthly costs ($12-15/month minimum even when idle). For simple CRUD apps with key-value access patterns, DynamoDB is cheaper and simpler. If you need complex SQL queries, joins, or relational data, use Aurora Serverless v2 instead.
How do I handle the AccessDenied error from CloudFront?
Set up Origin Access Control (OAC) in CloudFront, update the S3 bucket policy with the CloudFront service principal, and set the Default root object to index.html. This is the most common deployment issue — CloudFront can't read S3 without explicit OAC permissions. The auto-generated policy from CloudFront handles it.
Can I replace Cognito with another auth provider?
Yes. API Gateway supports any JWT-based authorizer. You can use Auth0, Firebase Auth, or Clerk instead of Cognito. Just configure the JWT authorizer with the new provider's issuer URL and audience/client ID. Cognito is cheapest for AWS-native applications (50,000 MAUs free), but Auth0 has a better developer experience.
Still Paying for Servers That Sit Idle 90% of the Time?
We've migrated 18 D2C internal tools from EC2/Heroku to serverless architectures in the last year. Average monthly savings: 89%. Average migration time: 3 weeks. If your dashboard, admin panel, or inventory system runs on always-on infrastructure, you're burning money. Let us audit your setup and show you what moves to serverless.
