Hands-On Project: Build a Serverless Application¶
Build a serverless REST API with authentication and a static frontend.
Estimated Time: 3-4 hours Difficulty: Intermediate Prerequisites: Basic programming (Node.js or Python), cloud account
Architecture Overview¶
CloudFront / CDN
|
S3 Static Website (Frontend - SPA)
|
API Gateway (REST API)
|
Cognito / Auth (JWT validation)
|
Lambda Functions (Business logic)
|
DynamoDB (NoSQL Database)
Cloud Equivalents
| Component | AWS | Azure | GCP |
|---|---|---|---|
| API Gateway | API Gateway | API Management | API Gateway |
| Functions | Lambda | Functions | Cloud Functions |
| Database | DynamoDB | Cosmos DB | Firestore |
| Auth | Cognito | Entra ID B2C | Firebase Auth |
| Static hosting | S3 + CloudFront | Static Web Apps | Cloud Storage + CDN |
Step 1: Create the Database¶
AWS DynamoDB¶
aws dynamodb create-table \
--table-name Items \
--attribute-definitions \
AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
--key-schema \
AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
# Add a Global Secondary Index for querying by type
aws dynamodb update-table \
--table-name Items \
--attribute-definitions AttributeName=itemType,AttributeType=S \
--global-secondary-index-updates '[{
"Create": {
"IndexName": "TypeIndex",
"KeySchema": [{"AttributeName": "itemType", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL"}
}
}]'
Azure Cosmos DB¶
az cosmosdb create --name my-serverless-db --resource-group myRG \
--capabilities EnableServerless
az cosmosdb sql database create --account-name my-serverless-db \
--resource-group myRG --name ItemsDB
az cosmosdb sql container create --account-name my-serverless-db \
--resource-group myRG --database-name ItemsDB --name Items \
--partition-key-path "/itemType"
GCP Firestore¶
gcloud firestore databases create --location=us-central
Step 2: Create Lambda Functions¶
List Items (GET /items)¶
# functions/list_items.py
import json
import boto3
from decimal import Decimal
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Items")
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def handler(event, context):
try:
# Get query parameters
params = event.get("queryStringParameters") or {}
item_type = params.get("type")
if item_type:
response = table.query(
IndexName="TypeIndex",
KeyConditionExpression="itemType = :t",
ExpressionAttributeValues={":t": item_type}
)
else:
response = table.scan()
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps(response["Items"], cls=DecimalEncoder)
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"error": str(e)})
}
Create Item (POST /items)¶
# functions/create_item.py
import json
import uuid
import boto3
from datetime import datetime
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Items")
def handler(event, context):
try:
# Get the authenticated user
claims = event["requestContext"]["authorizer"]["claims"]
user_id = claims["sub"]
body = json.loads(event["body"])
item = {
"PK": f"ITEM#{uuid.uuid4()}",
"SK": f"USER#{user_id}",
"itemType": body["type"],
"name": body["name"],
"description": body.get("description", ""),
"createdAt": datetime.utcnow().isoformat(),
"createdBy": user_id
}
table.put_item(Item=item)
return {
"statusCode": 201,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps({"message": "Item created", "id": item["PK"]})
}
except KeyError as e:
return {
"statusCode": 400,
"body": json.dumps({"error": f"Missing field: {str(e)}"})
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"error": str(e)})
}
Deploy Lambda Functions¶
# Package the function
cd functions
zip list_items.zip list_items.py
zip create_item.zip create_item.py
# Create IAM role
aws iam create-role --role-name lambda-api-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach DynamoDB policy
aws iam attach-role-policy --role-name lambda-api-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam attach-role-policy --role-name lambda-api-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Create functions
aws lambda create-function \
--function-name list-items \
--runtime python3.12 \
--handler list_items.handler \
--role arn:aws:iam::123456789012:role/lambda-api-role \
--zip-file fileb://list_items.zip \
--timeout 10 \
--memory-size 256
aws lambda create-function \
--function-name create-item \
--runtime python3.12 \
--handler create_item.handler \
--role arn:aws:iam::123456789012:role/lambda-api-role \
--zip-file fileb://create_item.zip \
--timeout 10 \
--memory-size 256
Step 3: Set Up Authentication¶
AWS Cognito User Pool¶
# Create User Pool
aws cognito-idp create-user-pool \
--pool-name my-app-users \
--auto-verified-attributes email \
--username-attributes email \
--policies '{
"PasswordPolicy": {
"MinimumLength": 8,
"RequireUppercase": true,
"RequireLowercase": true,
"RequireNumbers": true,
"RequireSymbols": false
}
}'
# Create App Client
aws cognito-idp create-user-pool-client \
--user-pool-id us-east-1_xxxxx \
--client-name my-app-client \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH \
--supported-identity-providers COGNITO
# Create a domain for hosted UI
aws cognito-idp create-user-pool-domain \
--user-pool-id us-east-1_xxxxx \
--domain my-app-auth
Step 4: Create API Gateway¶
# Create REST API
aws apigateway create-rest-api --name my-serverless-api \
--endpoint-configuration types=REGIONAL
# Get root resource ID
ROOT_ID=$(aws apigateway get-resources --rest-api-id API_ID \
--query 'items[?path==`/`].id' --output text)
# Create /items resource
aws apigateway create-resource --rest-api-id API_ID \
--parent-id $ROOT_ID --path-part items
# Create GET method with Cognito authorizer
aws apigateway create-authorizer --rest-api-id API_ID \
--name CognitoAuth \
--type COGNITO_USER_POOLS \
--provider-arns arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_xxxxx \
--identity-source method.request.header.Authorization
# Create GET method
aws apigateway put-method --rest-api-id API_ID \
--resource-id ITEMS_ID \
--http-method GET \
--authorization-type COGNITO_USER_POOLS \
--authorizer-id AUTH_ID
# Integrate with Lambda
aws apigateway put-integration --rest-api-id API_ID \
--resource-id ITEMS_ID \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:list-items/invocations
# Enable CORS
aws apigateway put-method --rest-api-id API_ID \
--resource-id ITEMS_ID \
--http-method OPTIONS \
--authorization-type NONE
# Deploy
aws apigateway create-deployment --rest-api-id API_ID --stage-name prod
API Endpoint: https://API_ID.execute-api.us-east-1.amazonaws.com/prod
Step 5: Deploy the Frontend¶
Static Site on S3¶
# Create bucket
aws s3 mb s3://my-serverless-app-frontend
# Configure for static website hosting
aws s3 website s3://my-serverless-app-frontend \
--index-document index.html --error-document index.html
# Build and deploy the frontend
cd frontend
npm run build
aws s3 sync build/ s3://my-serverless-app-frontend --delete
CloudFront Distribution¶
aws cloudfront create-distribution \
--origin-domain-name my-serverless-app-frontend.s3.us-east-1.amazonaws.com \
--default-root-object index.html \
--default-cache-behavior '{
"TargetOriginId": "S3Origin",
"ViewerProtocolPolicy": "redirect-to-https",
"ForwardedValues": {"QueryString": false, "Cookies": {"Forward": "none"}}
}'
Frontend Authentication Code¶
// Using AWS Amplify for Cognito integration
import { Amplify } from 'aws-amplify';
import { signIn, signUp, fetchAuthSession } from 'aws-amplify/auth';
Amplify.configure({
Auth: {
Cognito: {
userPoolId: 'us-east-1_xxxxx',
userPoolClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
}
}
});
// Sign in
async function login(email, password) {
const result = await signIn({ username: email, password });
return result;
}
// Get JWT token for API calls
async function getToken() {
const session = await fetchAuthSession();
return session.tokens.idToken.toString();
}
// Call API with auth
async function fetchItems() {
const token = await getToken();
const response = await fetch('https://API_ID.execute-api.us-east-1.amazonaws.com/prod/items', {
headers: { Authorization: token }
});
return response.json();
}
Step 6: Azure and GCP Alternatives¶
Azure - Static Web Apps + Functions¶
# Create a Static Web App (includes Functions backend)
az staticwebapp create --name my-serverless-app \
--resource-group myRG --source https://github.com/user/repo \
--branch main --app-location "/frontend" --api-location "/api" \
--output-location "build"
GCP - Cloud Functions + API Gateway¶
# Deploy Cloud Function
gcloud functions deploy list-items \
--runtime python312 \
--trigger-http \
--entry-point handler \
--allow-unauthenticated
# Create API Gateway config
gcloud api-gateway api-configs create my-config \
--api my-api --openapi-spec openapi.yaml
gcloud api-gateway gateways create my-gateway \
--api my-api --api-config my-config --location us-central1
Verification Checklist¶
- DynamoDB table is created with correct key schema
- Lambda functions deploy and execute successfully
- API Gateway routes map to correct Lambda functions
- Cognito user pool is configured with an app client
- API requires authentication (returns 401 without a token)
- Frontend is accessible via CloudFront with HTTPS
- Frontend can sign in and make authenticated API calls
- CORS is configured correctly (no browser errors)
Cleanup¶
# Delete in reverse order
aws cloudfront delete-distribution --id DIST_ID
aws s3 rb s3://my-serverless-app-frontend --force
aws apigateway delete-rest-api --rest-api-id API_ID
aws lambda delete-function --function-name list-items
aws lambda delete-function --function-name create-item
aws cognito-idp delete-user-pool --user-pool-id us-east-1_xxxxx
aws dynamodb delete-table --table-name Items