Modern AWS development makes it easier to build cloud apps. AWS offers many tools to help developers create better applications. In this guide, we’ll show you how to use AWS for both frontend and backend work. You’ll see real examples using TypeScript and Python. We’ll walk through each part step by step.
AWS Amplify Basics for Modern Development
AWS Amplify is a key tool for modern AWS development. It helps you build cloud apps quickly. Think of it as your toolkit for making web and mobile apps. Let’s see what it can do and how it compares to other tools.
What Can Amplify Do?
Amplify makes development easier with these features:
- Quick AWS setup
- Ready-to-use code blocks
- Pre-built UI parts
- Easy website hosting
- Simple user login
- Ready-to-use API tools
- Support for big apps
- Easy AWS connections
How Does it Compare?
Firebase is good at:
- Easy setup
- Live updates
- Push notifications
- Free small projects
Vercel and Netlify are good at:
- Quick site updates
- Simple dev tools
- Fast loading
- Next.js projects
For instance, here’s a simple example of user login with Amplify:
import { Amplify, Auth } from 'aws-amplify';
import { CognitoUser } from '@aws-amplify/auth';
// Configure Amplify
Amplify.configure({
Auth: {
region: 'us-east-1',
userPoolId: 'us-east-1_yourUserPoolId',
userPoolWebClientId: 'yourWebClientId'
}
});
// Sign up function
async function signUp(email: string, password: string): Promise<CognitoUser> {
try {
const { user } = await Auth.signUp({
username: email,
password,
attributes: {
email
}
});
return user;
} catch (error) {
console.error('Error signing up:', error);
throw error;
}
}
// Sign in function
async function signIn(email: string, password: string): Promise<CognitoUser> {
try {
const user = await Auth.signIn(email, password);
return user;
} catch (error) {
console.error('Error signing in:', error);
throw error;
}
}
// Check authentication state
async function checkAuth(): Promise<boolean> {
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
}
Understanding AWS Lambda
Now let’s look at AWS Lambda. It runs your code without you having to manage servers. You just write code, and Lambda takes care of the rest.
What Lambda Does Best
Lambda makes development easier by offering:
- No server work needed
- Auto-scaling
- Pay for what you use
- Many coding languages
- Works with AWS tools
- Runs tasks at once
- Handles errors well
- Updates on its own
How Does it Compare?
Regular servers (EC2) are good for:
- Full control
- Long tasks
- Steady work
- Heavy computing
- System tweaks
Containers (ECS/EKS) work well for:
- Complex apps
- Custom settings
- Special software
- Steady speed
- Long-running apps
For example, here’s a Lambda function that handles different types of tasks:
import json
import boto3
from typing import Dict, Any
from datetime import datetime
def process_event(event: Dict[str, Any]) -> Dict[str, Any]:
"""Process different types of events based on the event type."""
event_type = event.get('type', 'unknown')
if event_type == 'data_processing':
return process_data(event.get('data', {}))
elif event_type == 'notification':
return send_notification(event.get('message', ''))
elif event_type == 'scheduled':
return run_scheduled_task(event)
else:
raise ValueError(f'Unknown event type: {event_type}')
def process_data(data: Dict[str, Any]) -> Dict[str, Any]:
"""Process data and store results in DynamoDB."""
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ProcessedData')
# Add processing timestamp
data['processed_at'] = datetime.utcnow().isoformat()
# Store in DynamoDB
table.put_item(Item=data)
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Data processed successfully',
'data': data
})
}
def send_notification(message: str) -> Dict[str, Any]:
"""Send notification using SNS."""
sns = boto3.client('sns')
response = sns.publish(
TopicArn='arn:aws:sns:region:account-id:topic-name',
Message=message
)
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Notification sent successfully',
'messageId': response['MessageId']
})
}
def run_scheduled_task(event: Dict[str, Any]) -> Dict[str, Any]:
"""Run scheduled maintenance task."""
# Add your scheduled task logic here
current_time = datetime.utcnow().isoformat()
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Scheduled task completed',
'completedAt': current_time
})
}
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""Main Lambda handler function."""
try:
return process_event(event)
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps({
'error': str(e)
})
}
Building Frontend Apps
Moving forward, let’s make a web app using TypeScript and Amplify. Subsequently, this example shows how to handle user data:
import React, { useState, useEffect } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { useAuthenticator } from '@aws-amplify/ui-react';
// GraphQL operations
const listUserData = `
query ListUserData {
listUserData {
items {
id
title
content
createdAt
updatedAt
}
}
}
`;
const createUserData = `
mutation CreateUserData($input: CreateUserDataInput!) {
createUserData(input: $input) {
id
title
content
createdAt
updatedAt
}
}
`;
interface UserData {
id: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
}
export const UserDataComponent: React.FC = () => {
const [userData, setUserData] = useState<UserData[]>([]);
const [newTitle, setNewTitle] = useState('');
const [newContent, setNewContent] = useState('');
const { user } = useAuthenticator();
// Fetch user data
useEffect(() => {
fetchUserData();
}, []);
async function fetchUserData() {
try {
const response: any = await API.graphql(graphqlOperation(listUserData));
setUserData(response.data.listUserData.items);
} catch (error) {
console.error('Error fetching user data:', error);
}
}
// Create new user data
async function handleCreateData() {
try {
const input = {
title: newTitle,
content: newContent,
owner: user.username
};
await API.graphql(graphqlOperation(createUserData, { input }));
// Clear form and refresh data
setNewTitle('');
setNewContent('');
fetchUserData();
} catch (error) {
console.error('Error creating user data:', error);
}
}
return (
<div>
<h2>User Data</h2>
{/* Create Form */}
<div>
<input
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
placeholder="Title"
/>
<textarea
value={newContent}
onChange={e => setNewContent(e.target.value)}
placeholder="Content"
/>
<button onClick={handleCreateData}>Create</button>
</div>
{/* Display Data */}
<div>
{userData.map(item => (
<div key={item.id}>
<h3>{item.title}</h3>
<p>{item.content}</p>
<small>Updated: {new Date(item.updatedAt).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
};
Making Backend Services
Furthermore, here’s how to build safe and fast services with Python and Lambda:
from typing import Dict, Any, Optional
import boto3
import json
import logging
from datetime import datetime
from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
class UserService:
def __init__(self):
self.dynamodb = boto3.resource('dynamodb')
self.table = self.dynamodb.Table('Users')
self.cognito = boto3.client('cognito-idp')
def get_user(self, user_id: str) -> Optional[Dict[str, Any]]:
"""Get user data from DynamoDB."""
try:
response = self.table.get_item(Key={'userId': user_id})
return response.get('Item')
except ClientError as e:
logger.error(f"Error getting user {user_id}: {str(e)}")
raise
def update_user(self, user_id: str, updates: Dict[str, Any]) -> Dict[str, Any]:
"""Update user data in DynamoDB."""
update_expression = "SET "
expression_values = {}
for key, value in updates.items():
update_expression += f"#{key} = :{key}, "
expression_values[f":{key}"] = value
# Add updated timestamp
update_expression += "#updatedAt = :updatedAt"
expression_values[":updatedAt"] = datetime.utcnow().isoformat()
# Create expression attribute names
expression_names = {
f"#{k}": k for k in updates.keys()
}
expression_names["#updatedAt"] = "updatedAt"
try:
response = self.table.update_item(
Key={'userId': user_id},
UpdateExpression=update_expression,
ExpressionAttributeValues=expression_values,
ExpressionAttributeNames=expression_names,
ReturnValues="ALL_NEW"
)
return response['Attributes']
except ClientError as e:
logger.error(f"Error updating user {user_id}: {str(e)}")
raise
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""Handle API Gateway requests."""
try:
# Initialize service
user_service = UserService()
# Get HTTP method and path parameters
http_method = event['httpMethod']
path_parameters = event.get('pathParameters', {})
# Handle different HTTP methods
if http_method == 'GET':
user_id = path_parameters.get('userId')
if not user_id:
raise ValueError("User ID is required")
user = user_service.get_user(user_id)
if not user:
return {
'statusCode': 404,
'body': json.dumps({'message': 'User not found'})
}
return {
'statusCode': 200,
'body': json.dumps(user)
}
elif http_method == 'PUT':
user_id = path_parameters.get('userId')
if not user_id:
raise ValueError("User ID is required")
# Parse request body
body = json.loads(event['body'])
# Update user
updated_user = user_service.update_user(user_id, body)
return {
'statusCode': 200,
'body': json.dumps(updated_user)
}
else:
return {
'statusCode': 405,
'body': json.dumps({'message': 'Method not allowed'})
}
except ValueError as e:
return {
'statusCode': 400,
'body': json.dumps({'message': str(e)})
}
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({'message': 'Internal server error'})
}
Testing Your Code
After that, let’s look at how to test your Lambda code effectively:
import pytest
from unittest.mock import Mock, patch
from myservice import UserService
@pytest.fixture
def user_service():
"""Create a UserService instance with mocked AWS services."""
with patch('boto3.resource') as mock_resource, \
patch('boto3.client') as mock_client:
# Mock DynamoDB table
mock_table = Mock()
mock_resource.return_value.Table.return_value = mock_table
# Create service instance
service = UserService()
# Add mocks to service for testing
service._table = mock_table
yield service
def test_get_user_success(user_service):
"""Test successful user retrieval."""
# Arrange
user_id = "test-user-id"
expected_user = {
"userId": user_id,
"name": "Test User",
"email": "test@example.com"
}
user_service._table.get_item.return_value = {"Item": expected_user}
# Act
result = user_service.get_user(user_id)
# Assert
assert result == expected_user
user_service._table.get_item.assert_called_once_with(
Key={'userId': user_id}
)
Wrapping Up
Modern AWS development needs several key skills. In this guide, you’ve learned how to:
- Build web apps with React and Amplify
- Create safe Lambda functions
- Test your code well
- Keep your API safe
- Make your code fast
- Pick the right tools
Remember these tips for better AWS development:
- Keep security first
- Handle errors well
- Test everything
- Follow AWS guides
These skills will help you build better apps with AWS. As AWS grows, you’ll be ready to learn and try new things.