DEV Community

Randika Madhushan Perera
Randika Madhushan Perera

Posted on

Full Stack Application Hosting in AWS

Todo Application Documentation

Table of Contents

  1. Introduction
  2. Architecture Overview
  3. Backend Setup
  4. Frontend Setup
  5. AWS Configuration
  6. Deployment
  7. Troubleshooting
  8. Maintenance and Updates

1. Introduction

This document provides a comprehensive guide for setting up, deploying, and maintaining a full-stack Todo application using Node.js, React, Express, and MongoDB. The application is deployed on AWS, utilizing services such as Lambda, API Gateway, S3, and CloudFront.

2. Architecture Overview

  • Backend: Node.js with Express, deployed as an AWS Lambda function
  • Frontend: React, built with Vite, hosted on S3 and served via CloudFront
  • Database: MongoDB Atlas
  • API: AWS API Gateway
  • Authentication: (To be implemented)

3. Backend Setup

3.1 Lambda Function

Create a new file named index.js:

const mongoose = require('mongoose');

let cachedDb = null;

async function connectToDatabase() {
  if (cachedDb) {
    return cachedDb;
  }

  const connection = await mongoose.connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  cachedDb = connection;
  return connection;
}

const todoSchema = new mongoose.Schema({
  title: { type: String, required: true },
  completed: { type: Boolean, default: false },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
});

const Todo = mongoose.model('Todo', todoSchema);

exports.handler = async (event) => {
  const corsHeaders = {
    'Access-Control-Allow-Origin': 'https://your-cloudfront-domain.cloudfront.net',
    'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
    'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
    'Access-Control-Allow-Credentials': 'true'
  };

  if (event.httpMethod === 'OPTIONS') {
    return {
      statusCode: 200,
      headers: corsHeaders,
      body: JSON.stringify({ message: 'CORS preflight request successful' })
    };
  }

  try {
    await connectToDatabase();

    const { httpMethod, resource, pathParameters, body } = event;

    switch (`\${httpMethod} \${resource}`) {
      case 'GET /todos':
        const todos = await Todo.find().sort({ createdAt: -1 });
        return { 
          statusCode: 200, 
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify(todos) 
        };

      case 'POST /todos':
        const newTodo = new Todo(JSON.parse(body));
        const savedTodo = await newTodo.save();
        return { 
          statusCode: 201, 
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify(savedTodo) 
        };

      case 'PUT /todos/{id}':
        const updatedTodo = await Todo.findByIdAndUpdate(
          pathParameters.id,
          { ...JSON.parse(body), updatedAt: Date.now() },
          { new: true }
        );
        if (!updatedTodo) {
          return { 
            statusCode: 404, 
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
            body: JSON.stringify({ message: 'Todo not found' }) 
          };
        }
        return { 
          statusCode: 200, 
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify(updatedTodo) 
        };

      case 'DELETE /todos/{id}':
        const deletedTodo = await Todo.findByIdAndDelete(pathParameters.id);
        if (!deletedTodo) {
          return { 
            statusCode: 404, 
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
            body: JSON.stringify({ message: 'Todo not found' }) 
          };
        }
        return { 
          statusCode: 200, 
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify({ message: 'Todo deleted successfully' }) 
        };

      default:
        return { 
          statusCode: 400, 
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify({ message: 'Invalid request' }) 
        };
    }
  } catch (error) {
    console.error('Error:', error);
    return { 
      statusCode: 500, 
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: 'Internal server error' }) 
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

3.2 Package Configuration

Create a package.json file:

{
  "name": "todo-api-lambda",
  "version": "1.0.0",
  "description": "Todo API Lambda Function",
  "main": "index.js",
  "dependencies": {
    "mongoose": "^6.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

3.3 Deployment Package

  1. Install dependencies: npm install
  2. Create a ZIP file: zip -r function.zip index.js node_modules

4. Frontend Setup

4.1 Create React App

  1. Create a new Vite project: npm create vite@latest client -- --template react
  2. Navigate to the project directory: cd client
  3. Install dependencies: npm install

4.2 API Service

Create a file named src/services/todoService.js:

import axios from 'axios';

const API_URL = 'https://your-api-gateway-url.execute-api.us-west-2.amazonaws.com/prod/todos';

const api = axios.create({
  baseURL: API_URL,
  withCredentials: true
});

export const getTodos = async () => {
  const response = await api.get('');
  return response.data;
};

export const createTodo = async (title) => {
  const response = await api.post('', { title });
  return response.data;
};

export const updateTodo = async (id, updates) => {
  const response = await api.put(`/\${id}`, updates);
  return response.data;
};

export const deleteTodo = async (id) => {
  await api.delete(`/\${id}`);
};
Enter fullscreen mode Exit fullscreen mode

4.3 Main App Component

Update src/App.jsx:

import React, { useState, useEffect } from 'react';
import { getTodos, createTodo, updateTodo, deleteTodo } from './services/todoService';
import './App.css';

function App() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    const fetchedTodos = await getTodos();
    setTodos(fetchedTodos);
  };

  const handleCreateTodo = async (e) => {
    e.preventDefault();
    if (newTodo.trim()) {
      const createdTodo = await createTodo(newTodo);
      setTodos([createdTodo, ...todos]);
      setNewTodo('');
    }
  };

  const handleUpdateTodo = async (id, updates) => {
    const updatedTodo = await updateTodo(id, updates);
    setTodos(todos.map(todo => todo._id === id ? updatedTodo : todo));
  };

  const handleDeleteTodo = async (id) => {
    await deleteTodo(id);
    setTodos(todos.filter(todo => todo._id !== id));
  };

  return (
    <div className="App">
      <h1>Todo App</h1>
      <form onSubmit={handleCreateTodo}>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
        />
        <button type="submit">Add Todo</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo._id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleUpdateTodo(todo._id, { completed: !todo.completed })}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.title}
            </span>
            <button onClick={() => handleDeleteTodo(todo._id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

5. AWS Configuration

5.1 Lambda Function

  1. Create a new Lambda function in the AWS Console.
  2. Upload the ZIP file created in step 3.3.
  3. Set the handler to index.handler.
  4. Add environment variable: MONGODB_URI with your MongoDB connection string.

5.2 API Gateway

  1. Create a new API in API Gateway.
  2. Create resources and methods for /todos and /todos/{id}.
  3. Integrate each method with your Lambda function.
  4. Enable CORS for each method.
  5. Deploy the API to a new stage (e.g., "prod").

5.3 S3 and CloudFront

  1. Create an S3 bucket for hosting the React app.
  2. Create a CloudFront distribution with the S3 bucket as the origin.
  3. Set up Origin Access Control (OAC) for secure access to the S3 bucket.

6. Deployment

6.1 Backend Deployment

Update the Lambda function code:

zip -r function.zip index.js node_modules
aws lambda update-function-code --function-name YourFunctionName --zip-file fileb://function.zip
Enter fullscreen mode Exit fullscreen mode

6.2 Frontend Deployment

  1. Build the React app: npm run build
  2. Upload to S3:
   aws s3 sync build/ s3://your-bucket-name --delete
Enter fullscreen mode Exit fullscreen mode
  1. Invalidate CloudFront cache:
   aws cloudfront create-invalidation --distribution-id YourDistributionID --paths "/*"
Enter fullscreen mode Exit fullscreen mode

7. Troubleshooting

7.1 CORS Issues

If encountering CORS errors:

  1. Ensure CORS is enabled in API Gateway for all methods.
  2. Verify CORS headers in the Lambda function response.
  3. Check that the Access-Control-Allow-Origin header matches your CloudFront domain.

7.2 API Gateway 5XX Errors

  1. Check Lambda function logs in CloudWatch.
  2. Verify that the Lambda function has the correct permissions to access other AWS services.

7.3 MongoDB Connection Issues

  1. Ensure the MONGODB_URI environment variable is set correctly in Lambda.
  2. Verify that the Lambda function has network access to MongoDB Atlas (may require VPC configuration).

8. Maintenance and Updates

8.1 Updating the Backend

  1. Make changes to the Lambda function code.
  2. Redeploy using the steps in section 6.1.

8.2 Updating the Frontend

  1. Make changes to the React application.
  2. Rebuild and redeploy using the steps in section 6.2.

8.3 Monitoring

  1. Use CloudWatch to monitor Lambda function performance and errors.
  2. Set up CloudWatch Alarms for critical metrics.

8.4 Scaling

  1. Adjust Lambda function memory and timeout settings as needed.
  2. Consider implementing caching at the API Gateway level for frequently accessed data.

This documentation provides a comprehensive guide for setting up, deploying, and maintaining your Todo application. Remember to keep your dependencies updated and regularly review AWS best practices for potential improvements to your architecture.

Top comments (0)