A serverless web application built with AWS Lambda that processes form submissions and stores them in DynamoDB, displaying both form results and existing database records in real-time.
- Overview
- Architecture
- Prerequisites
- Step-by-Step Setup
- Project Structure
- Code Files
- Testing
- Troubleshooting
- Features
- Security Considerations
This project demonstrates:
- Serverless form processing using AWS Lambda
- Database integration with DynamoDB
- Dynamic HTML generation server-side
- Real-time data display of form submissions
- HTTP Function URLs for direct web access
Internet β Lambda Function URL β Lambda Function β DynamoDB β Dynamic HTML Response
Components:
- AWS Lambda: Processes HTTP requests and generates dynamic HTML
- DynamoDB: Stores form submissions with composite keys
- IAM Role: Provides necessary permissions for Lambda-DynamoDB interaction
- Function URL: Enables direct HTTP access to the Lambda function
- AWS Account with appropriate permissions
- Basic knowledge of AWS Lambda, DynamoDB, and IAM
- Text editor for code editing
- Web browser for testing
-
Navigate to DynamoDB Console
- Go to DynamoDB Console
-
Create Table
- Click "Create table"
- Table name:
formStore - Partition key:
PK(String) - Sort key:
SK(String) - Settings: Use default settings (On-demand billing)
- Click "Create table"
-
Wait for Table Creation
- Status should show "Active" before proceeding
-
Navigate to Lambda Console
- Go to Lambda Console
-
Create Function
- Click "Create function"
- Choose "Author from scratch"
- Function name:
http-function-url-tutorial - Runtime:
Node.js 22.x - Click "Create function"
-
Add Function URL
- In your Lambda function, go to Configuration β Function URL
- Click "Create function URL"
- Auth type:
NONE(for public access) - CORS: Enable if needed
- Click "Save"
-
Copy Function URL
- Save the generated URL for testing
-
Navigate to IAM Console
- Go to IAM Roles
-
Find Lambda Execution Role
- Search for your Lambda function's role (e.g.,
http-function-url-tutorial-role-xxxxx)
- Search for your Lambda function's role (e.g.,
-
Add DynamoDB Policy
- Click on the role β Add permissions β Create inline policy
- Policy name:
DynamoDBAccessPolicy - JSON Policy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "dynamodb:PutItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:GetItem" ], "Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/formStore" } ] }
- Replace YOUR_ACCOUNT_ID with your actual AWS account ID
- Click "Create policy"
- Create these files locally on your computer:
5.1 Create index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Form Handler with Database</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.result {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<div class="container">
<h1>π Dynamic Form Handler with Database</h1>
<p>Submit the form below to see server-side processing and DynamoDB storage:</p>
{formResults}
<form method="GET">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" placeholder="Enter your name" required>
</div>
<div class="form-group">
<label for="location">Location:</label>
<input type="text" id="location" name="location" placeholder="Enter your location" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" placeholder="Enter your email">
</div>
<div class="form-group">
<label for="category">Category:</label>
<select id="category" name="category">
<option value="">Select a category</option>
<option value="general">General Inquiry</option>
<option value="support">Support Request</option>
<option value="feedback">Feedback</option>
</select>
</div>
<button type="submit">Submit Form</button>
</form>
<hr style="margin: 30px 0;">
{table}
</div>
</body>
</html>5.2 Create index.js
const fs = require('fs');
const path = require('path');
// Read HTML file at startup
const html = fs.readFileSync(path.join(__dirname, 'index.html'), { encoding: 'utf8' });
// AWS SDK v3 imports
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, PutCommand, ScanCommand } = require('@aws-sdk/lib-dynamodb');
// Create DynamoDB client
const client = new DynamoDBClient({ region: process.env.AWS_REGION || 'us-east-1' });
const dynamo = DynamoDBDocumentClient.from(client);
exports.handler = async (event, context) => {
try {
console.log('π Handler started');
console.log('Event:', JSON.stringify(event, null, 2));
// First, get existing records from DynamoDB
let tableQuery = null;
try {
console.log('π Scanning DynamoDB table...');
const scanCommand = new ScanCommand({
TableName: "formStore"
});
tableQuery = await dynamo.send(scanCommand);
console.log('β
DynamoDB Scan Success!');
console.log('π Records found:', tableQuery.Count);
} catch (scanError) {
console.error('β DynamoDB Scan Error:', scanError);
tableQuery = { Items: [] };
}
// Process form data and save to DynamoDB if present
if (event.queryStringParameters && Object.keys(event.queryStringParameters).length > 0) {
try {
console.log('πΎ Saving new form data...');
const putCommand = new PutCommand({
TableName: "formStore",
Item: {
PK: "form",
SK: context.awsRequestId,
timestamp: new Date().toISOString(),
sourceIP: event.requestContext?.http?.sourceIp || 'unknown',
userAgent: event.headers?.['user-agent'] || 'unknown',
form: event.queryStringParameters
}
});
await dynamo.send(putCommand);
console.log('β
Successfully saved to DynamoDB:', context.awsRequestId);
// Refresh the table data after inserting new record
const refreshScanCommand = new ScanCommand({
TableName: "formStore"
});
tableQuery = await dynamo.send(refreshScanCommand);
console.log('π Refreshed data, new count:', tableQuery.Count);
} catch (dbError) {
console.error('β DynamoDB Put Error:', dbError);
}
}
// Apply dynamic functions
console.log('π§ Processing HTML template...');
let modifiedHTML = dynamicForm(html, event.queryStringParameters);
modifiedHTML = dynamictable(modifiedHTML, tableQuery);
console.log('β
HTML processing complete');
const response = {
statusCode: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache'
},
body: modifiedHTML,
};
return response;
} catch (error) {
console.error('π₯ Handler Error:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Internal Server Error',
message: error.message,
requestId: context.awsRequestId
})
};
}
};
function dynamicForm(html, queryStringParameters) {
let formResults = '';
if (queryStringParameters && Object.keys(queryStringParameters).length > 0) {
formResults = '<div class="result"><h4>β
Form Submission Saved Successfully!</h4><ul>';
Object.entries(queryStringParameters).forEach(([key, value]) => {
if (value && value.trim() !== '') {
formResults += `<li><strong>${key}:</strong> ${value}</li>`;
}
});
formResults += '</ul><p><em>Data has been stored in DynamoDB table "formStore"</em></p></div>';
} else {
formResults = '<div class="result" style="background: #fff3cd; border-color: #ffeaa7; color: #856404;"><h4>βΉοΈ Ready to Process Form Data</h4><p>Fill out the form below to see dynamic processing in action!</p></div>';
}
return html.replace('{formResults}', formResults);
}
function dynamictable(html, tableQuery) {
console.log('ποΈ Building table HTML...');
let table = "";
if (tableQuery && tableQuery.Items && tableQuery.Items.length > 0) {
console.log('β
Found items, building table...');
// Create a proper HTML table
table = `
<div style="overflow-x: auto; margin: 15px 0; border: 2px solid #007bff; border-radius: 8px; padding: 15px; background: #f8f9fa;">
<h5 style="color: #007bff; margin-top: 0;">π Form Submissions Database</h5>
<table style="width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 14px; background: white;">
<thead>
<tr style="background-color: #007bff; color: white;">
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Submission ID</th>
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Timestamp</th>
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Name</th>
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Location</th>
<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">Details</th>
</tr>
</thead>
<tbody>
`;
for (let i = 0; i < tableQuery.Items.length; i++) {
const item = tableQuery.Items[i];
const formData = item.form || {};
table += `
<tr style="background-color: ${i % 2 === 0 ? '#ffffff' : '#f9f9f9'};">
<td style="border: 1px solid #ddd; padding: 8px; font-family: monospace; font-size: 10px;">${(item.SK || 'N/A').substring(0, 8)}...</td>
<td style="border: 1px solid #ddd; padding: 8px;">${item.timestamp ? new Date(item.timestamp).toLocaleString() : 'N/A'}</td>
<td style="border: 1px solid #ddd; padding: 8px;"><strong>${formData.name || 'N/A'}</strong></td>
<td style="border: 1px solid #ddd; padding: 8px;">${formData.location || 'N/A'}</td>
<td style="border: 1px solid #ddd; padding: 8px;">
<details>
<summary style="cursor: pointer; color: #007bff;">View All</summary>
<pre style="margin: 5px 0; font-size: 11px; background: #f8f9fa; padding: 5px; border-radius: 3px;">${JSON.stringify(formData, null, 2)}</pre>
</details>
</td>
</tr>
`;
}
table += `
</tbody>
</table>
<p style="color: #6c757d; font-size: 12px; margin-bottom: 0;"><em>π Total records: ${tableQuery.Items.length} | Last updated: ${new Date().toLocaleString()}</em></p>
</div>
`;
console.log('β
Table HTML built successfully');
} else {
console.log('β οΈ No items found');
table = '<div style="padding: 20px; background: #f8f9fa; border-radius: 5px; text-align: center; color: #6c757d; border: 2px dashed #dee2e6;"><p>π No records found in DynamoDB table.</p><p><em>Submit the form above to create your first record!</em></p></div>';
}
return html.replace("{table}", `<h4>π DynamoDB Records:</h4>${table}`);
}-
Create Deployment Package
- Create a folder named
lambda-function. - Add both
index.jsandindex.htmlto the folder. - Select both files and create a ZIP archive.
- Create a folder named
-
Upload to Lambda
- Go to your Lambda function in the AWS Console.
- In the Code source section, click Upload from β .zip file.
-
Deploy
- Upload your ZIP file.
- Click Deploy.
-
Access Function URL
- Use the Function URL created in your Lambda settings.
- Open the URL in your web browser.
-
Test Form Submission
- Fill out the form with sample data.
- Click Submit Form.
- Verify the data appears in the results table on the page.
-
Verify DynamoDB
- Go to the DynamoDB Console.
- Click Explore table items.
- Confirm your submissions are stored in the
formStoretable. - Check that the database table shows the new record
lambda-function/
βββ index.js # Lambda function logic (Node.js)
βββ index.html # Frontend template
βββ README.md # Documentation
- Form Submission: Fill out the form and submit to ensure data is captured.
- Data Persistence: Refresh the page to see if stored records appear in the table.
- Error Handling: Submit an empty form to test client/server-side validation.
- Monitor Logs: Open the CloudWatch console to track execution.
- Identify Issues: Look specifically for success confirmations or error stack traces in the log streams.
- Error:
AccessDeniedException: User is not authorized to perform: dynamodb:Scan - Solution: Go to the IAM Role attached to your Lambda and add a policy that allows
dynamodb:Scananddynamodb:PutItem.
- Verify: Ensure the Function URL is generated in the Lambda configuration.
- Auth Type: Check that the Authorization type is set to
NONEfor public access. - Connectivity: Try accessing the URL from an incognito browser window.
- Permissions: Confirm the IAM role includes
dynamodb:Scan. - Template Check: Ensure your
index.htmlcontains the{table}placeholder for the Lambda function to inject data. - Logs: Check CloudWatch for specific scan failure errors.
- Permissions: Confirm the IAM role includes
dynamodb:PutItem. - Table Name: Verify the table name in your code matches your DynamoDB table exactly (e.g.,
formStore). - PUT Errors: Review logs for validation exceptions or throughput issues.
- Check CloudWatch Logs for detailed error messages.
- Verify IAM Permissions in the IAM console.
- Test DynamoDB Access directly using the AWS Console or CLI.
- Validate the HTML Template contains all required placeholders.
- π Dynamic Form Processing: Real-time form submission handling.
- πΎ Database Integration: Automatic storage in DynamoDB.
- π Live Data Display: Shows existing records in a formatted table.
- π Real-time Updates: Table refreshes after new submissions.
- π± Responsive Design: Works on desktop and mobile devices.
- π‘οΈ Error Handling: Graceful error catching and logging.
- π¨ Modern UI: Clean, professional interface.
- IAM Permissions: Always follow the principle of least privilege.
- Input Validation: Validate form inputs on both client and server side.
- CORS Configuration: Configure CORS settings as needed for your domain.
- Authentication: Consider adding AWS Cognito for production environments.
- Rate Limiting: Implement limits to prevent API abuse or high costs.
- Data Encryption: Enable encryption at rest for DynamoDB.
- AWS Lambda Documentation
- DynamoDB Developer Guide
- IAM User Guide
- AWS SDK for JavaScript v3
- Getting Started with Lambda HTTP
Dynamic form with real-time validation and modern UI
Confirmation message after successful form submission
Live display of all form submissions from DynamoDB
Lambda function code and configuration in AWS Console
Form data stored in DynamoDB with composite keys
Function execution logs showing successful operations
Built with β€οΈ using AWS Lambda, DynamoDB, and Node.js