Complete Serverless Architecture
· 12 min · Complete project breakdown
serverless.yml Configuration
service: urlstatuschecker-api
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
STAGE: ${self:provider.stage}
USERS_TABLE: ${self:service}-users-${self:provider.stage}
CHECKS_TABLE: ${self:service}-checks-${self:provider.stage}
LIMITS_TABLE: ${self:service}-limits-${self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:provider.environment.USERS_TABLE}"
- "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:provider.environment.CHECKS_TABLE}"
- "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:provider.environment.LIMITS_TABLE}"
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource: "arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/urlchecker/*"
functions:
checkUrl:
handler: src/handlers/checkUrl.handler
timeout: 30
events:
- http:
path: /check
method: post
cors: true
authorizer: auth
batchCheck:
handler: src/handlers/batchCheck.handler
timeout: 300
events:
- http:
path: /batch-check
method: post
cors: true
authorizer: auth
getUserData:
handler: src/handlers/getUserData.handler
events:
- http:
path: /user
method: get
cors: true
authorizer: auth
updateUserPlan:
handler: src/handlers/updateUserPlan.handler
events:
- http:
path: /user/plan
method: put
cors: true
authorizer: auth
processPayment:
handler: src/handlers/processPayment.handler
events:
- http:
path: /payment
method: post
cors: true
auth:
handler: src/handlers/auth.handler
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.USERS_TABLE}
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
ChecksTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.CHECKS_TABLE}
AttributeDefinitions:
- AttributeName: checkId
AttributeType: S
- AttributeName: userId
AttributeType: S
- AttributeName: timestamp
AttributeType: N
KeySchema:
- AttributeName: checkId
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: UserTimeIndex
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: timestamp
KeyType: RANGE
Projection:
ProjectionType: ALL
BillingMode: PAY_PER_REQUEST
LimitsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.LIMITS_TABLE}
AttributeDefinitions:
- AttributeName: limitKey
AttributeType: S
KeySchema:
- AttributeName: limitKey
KeyType: HASH
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
BillingMode: PAY_PER_REQUEST
Lambda Functions
1. URL Check Handler
// src/handlers/checkUrl.js
const AWS = require('aws-sdk');
const fetch = require('node-fetch');
const { v4: uuidv4 } = require('uuid');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
try {
const { url } = JSON.parse(event.body);
const userId = event.requestContext.authorizer.principalId;
// Validate URL
if (!isValidUrl(url)) {
return errorResponse(400, 'Invalid URL format');
}
// Check rate limits
await checkRateLimit(userId);
// Perform URL check
const startTime = Date.now();
const result = await checkUrlStatus(url);
const responseTime = Date.now() - startTime;
// Save to DynamoDB
const checkRecord = {
checkId: uuidv4(),
userId,
url,
status: result.status,
statusText: result.statusText,
responseTime,
timestamp: Date.now(),
headers: result.headers
};
await dynamodb.put({
TableName: process.env.CHECKS_TABLE,
Item: checkRecord
}).promise();
// Update user usage
await updateUserUsage(userId);
return successResponse({
url,
status: result.status,
statusText: result.statusText,
responseTime,
timestamp: checkRecord.timestamp
});
} catch (error) {
console.error('Error checking URL:', error);
return errorResponse(500, 'Internal server error');
}
};
async function checkUrlStatus(url) {
try {
const response = await fetch(url, {
method: 'HEAD',
timeout: 10000,
follow: 5,
headers: {
'User-Agent': 'URLStatusChecker/1.0 (+https://urlstatuschecker.com)'
}
});
return {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers)
};
} catch (error) {
return {
status: 0,
statusText: error.message,
headers: {}
};
}
}
2. Batch Check Handler
// src/handlers/batchCheck.js
exports.handler = async (event) => {
try {
const { urls } = JSON.parse(event.body);
const userId = event.requestContext.authorizer.principalId;
// Validate batch size
const userLimits = await getUserLimits(userId);
if (urls.length > userLimits.batchSize) {
return errorResponse(400, `Batch size exceeds limit of ${userLimits.batchSize}`);
}
// Check monthly usage
const usage = await getMonthlyUsage(userId);
if (usage + urls.length > userLimits.monthlyLimit) {
return errorResponse(429, 'Monthly usage limit exceeded');
}
// Process URLs in chunks to avoid timeout
const chunkSize = 10;
const results = [];
for (let i = 0; i < urls.length; i += chunkSize) {
const chunk = urls.slice(i, i + chunkSize);
const chunkPromises = chunk.map(url => checkUrlWithRetry(url));
const chunkResults = await Promise.allSettled(chunkPromises);
results.push(...chunkResults.map((result, index) => ({
url: chunk[index],
...result.value || { status: 0, statusText: 'Check failed' }
})));
// Rate limiting between chunks
if (i + chunkSize < urls.length) {
await delay(1000);
}
}
// Bulk insert to DynamoDB
await saveBatchResults(userId, results);
return successResponse({
totalChecked: results.length,
results: results
});
} catch (error) {
console.error('Batch check error:', error);
return errorResponse(500, 'Batch check failed');
}
};
3. User Data Handler
// src/handlers/getUserData.js
exports.handler = async (event) => {
try {
const userId = event.requestContext.authorizer.principalId;
// Get user profile
const user = await dynamodb.get({
TableName: process.env.USERS_TABLE,
Key: { userId }
}).promise();
if (!user.Item) {
return errorResponse(404, 'User not found');
}
// Get monthly usage
const usage = await getMonthlyUsage(userId);
// Get recent checks
const recentChecks = await dynamodb.query({
TableName: process.env.CHECKS_TABLE,
IndexName: 'UserTimeIndex',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: {
':userId': userId
},
ScanIndexForward: false,
Limit: 50
}).promise();
const userData = {
...user.Item,
usage: {
monthly: usage,
remaining: getUserLimits(user.Item.plan).monthlyLimit - usage
},
recentChecks: recentChecks.Items
};
// Remove sensitive data
delete userData.password;
delete userData.stripeCustomerId;
return successResponse(userData);
} catch (error) {
console.error('Get user data error:', error);
return errorResponse(500, 'Failed to get user data');
}
};
4. Payment Processing
// src/handlers/processPayment.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
exports.handler = async (event) => {
try {
const { planId, paymentMethodId, userId } = JSON.parse(event.body);
const plans = {
pro: { price: 2.99, name: 'Pro Plan' },
business: { price: 9.99, name: 'Business Plan' }
};
if (!plans[planId]) {
return errorResponse(400, 'Invalid plan selected');
}
// Create or retrieve customer
let customer = await getStripeCustomer(userId);
if (!customer) {
customer = await stripe.customers.create({
metadata: { userId }
});
await saveStripeCustomer(userId, customer.id);
}
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{
price_data: {
currency: 'usd',
product_data: {
name: plans[planId].name
},
unit_amount: Math.round(plans[planId].price * 100),
recurring: {
interval: 'month'
}
}
}],
default_payment_method: paymentMethodId,
expand: ['latest_invoice.payment_intent']
});
// Update user plan
await dynamodb.update({
TableName: process.env.USERS_TABLE,
Key: { userId },
UpdateExpression: 'SET plan = :plan, stripeSubscriptionId = :subId, updatedAt = :now',
ExpressionAttributeValues: {
':plan': planId,
':subId': subscription.id,
':now': new Date().toISOString()
}
}).promise();
return successResponse({
subscriptionId: subscription.id,
status: subscription.status,
clientSecret: subscription.latest_invoice.payment_intent.client_secret
});
} catch (error) {
console.error('Payment processing error:', error);
return errorResponse(500, 'Payment processing failed');
}
};
5. Authorization Handler
// src/handlers/auth.js
const jwt = require('jsonwebtoken');
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
exports.handler = async (event) => {
try {
const token = event.authorizationToken.replace('Bearer ', '');
// Verify JWT token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.sub;
// Generate policy
return generatePolicy(userId, 'Allow', event.methodArn);
} catch (error) {
console.error('Auth error:', error);
return generatePolicy('unauthorized', 'Deny', event.methodArn);
}
};
function generatePolicy(principalId, effect, resource) {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource
}]
}
};
}
Frontend Components
URL Checker Interface
// js/URLChecker.js
class URLChecker {
constructor() {
this.apiBase = 'https://api.urlstatuschecker.com';
this.authToken = localStorage.getItem('authToken');
this.init();
}
init() {
this.setupEventListeners();
this.loadUserData();
}
setupEventListeners() {
// Single URL check
document.getElementById('single-check-btn').addEventListener('click', () => {
const url = document.getElementById('url-input').value;
this.checkSingleUrl(url);
});
// Batch URL check
document.getElementById('batch-check-btn').addEventListener('click', () => {
const urls = this.parseUrlList(document.getElementById('url-list').value);
this.checkBatchUrls(urls);
});
// Export results
document.getElementById('export-btn').addEventListener('click', () => {
this.exportResults();
});
}
async checkSingleUrl(url) {
this.showLoading(true);
try {
const response = await fetch(`${this.apiBase}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
body: JSON.stringify({ url })
});
const result = await response.json();
this.displaySingleResult(result);
} catch (error) {
this.showError('Failed to check URL');
} finally {
this.showLoading(false);
}
}
async checkBatchUrls(urls) {
this.showBatchProgress(0, urls.length);
try {
const response = await fetch(`${this.apiBase}/batch-check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
body: JSON.stringify({ urls })
});
const result = await response.json();
this.displayBatchResults(result.results);
} catch (error) {
this.showError('Batch check failed');
}
}
}
Pricing Component
// js/PricingManager.js
class PricingManager {
constructor() {
this.stripe = Stripe('pk_live_...');
this.plans = {
free: { price: 0, checks: 500, batch: 10 },
pro: { price: 2.99, checks: 10000, batch: 50 },
business: { price: 9.99, checks: 100000, batch: 100 }
};
this.init();
}
init() {
this.renderPricingCards();
this.setupPlanSelection();
}
renderPricingCards() {
const container = document.getElementById('pricing-cards');
Object.entries(this.plans).forEach(([planId, plan]) => {
const card = this.createPricingCard(planId, plan);
container.appendChild(card);
});
}
createPricingCard(planId, plan) {
const card = document.createElement('div');
card.className = `pricing-card ${planId === 'pro' ? 'featured' : ''}`;
card.innerHTML = `
${planId.charAt(0).toUpperCase() + planId.slice(1)}
$
${plan.price}
/month
✓ ${plan.checks.toLocaleString()} monthly checks
✓ ${plan.batch} URLs per batch
✓ Export results
${planId !== 'free' ? '✓ Favorites & history' : ''}
${planId === 'business' ? '✓ API access' : ''}
`;
return card;
}
async processPlanUpgrade(planId, paymentMethodId) {
try {
const response = await fetch('/api/payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
},
body: JSON.stringify({
planId,
paymentMethodId,
userId: this.getCurrentUserId()
})
});
const result = await response.json();
if (result.clientSecret) {
// 3D Secure authentication required
await this.stripe.confirmCardPayment(result.clientSecret);
}
this.showSuccess('Plan upgraded successfully!');
this.updateUserInterface(planId);
} catch (error) {
this.showError('Payment failed. Please try again.');
}
}
}
API Endpoints Summary
POST /check - Single URL check
POST /batch-check - Multiple URLs check
GET /user - Get user data and usage
PUT /user/plan - Update user subscription
POST /payment - Process Stripe payment
POST /auth - Google OAuth authentication
GET /usage - Get usage statistics
DELETE /user - Delete user account
Deployment Pipeline
# package.json scripts
{
"scripts": {
"deploy:dev": "serverless deploy --stage dev",
"deploy:prod": "serverless deploy --stage prod",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src/",
"tailwind:build": "tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify"
}
}
# Deployment process
npm run lint
npm run test
npm run build
npm run tailwind:build
npm run deploy:prod