Modern AWS Development: From Frontend to Backend

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:

  1. Build web apps with React and Amplify
  2. Create safe Lambda functions
  3. Test your code well
  4. Keep your API safe
  5. Make your code fast
  6. 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.