All Articles

AWS Amplify Beginner's Guide: Building a Full-Stack Todo App from Scratch

In this guide, we’ll build a full-stack todo application using AWS Amplify and React. The app will include user authentication, API integration, and real-time updates. By the end, you’ll have a production-ready application deployed on AWS.

Project Overview

We’ll build a todo app with the following features:

  • User authentication
  • CRUD operations for todos
  • Real-time updates
  • Data persistence with DynamoDB
  • Responsive UI with Material-UI

Prerequisites

  1. Node.js (v14.x or later)

    • Windows: Download from nodejs.org
    • macOS: brew install node
    • Linux: sudo apt install nodejs npm
  2. AWS Account

    • Free tier account at aws.amazon.com
    • Admin access for your IAM user
  3. Code Editor

    • VSCode recommended
    • Install AWS Toolkit extension

Development Environment Setup

Step 1: Install Amplify CLI

# For all operating systems
npm install -g @aws-amplify/cli

# Verify installation
amplify --version

Step 2: Configure Amplify

amplify configure

Follow the prompts:

  1. Select your region (e.g., us-east-1)
  2. Create new IAM user if needed
  3. Enter Access Key and Secret Access Key
  4. Name your profile

Project Setup

Step 1: Create React Application

npx create-react-app amplify-todo
cd amplify-todo

Step 2: Install Dependencies

npm install @aws-amplify/ui-react aws-amplify @mui/material @mui/icons-material @emotion/react @emotion/styled

Step 3: Initialize Amplify Project

amplify init

# Enter the following:
? Enter a name for the project: amplifytodo
? Enter a name for the environment: dev
? Choose your default editor: (Your preferred editor)
? Choose the type of app that you're building: javascript
? What javascript framework are you using: react
? Source Directory Path: src
? Distribution Directory Path: build
? Build Command: npm run build
? Start Command: npm start

Adding Authentication

Step 1: Add Auth Service

amplify add auth

# Use default configuration:
? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No

Step 2: Update React App with Auth

// src/App.js
import { Amplify } from 'aws-amplify';
import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import awsExports from './aws-exports';

Amplify.configure(awsExports);

function App({ signOut, user }) {
  return (
    <div>
      <h1>Hello {user.username}</h1>
      <button onClick={signOut}>Sign out</button>
    </div>
  );
}

export default withAuthenticator(App);

Adding API and Database

Step 1: Add API Service

amplify add api

# Enter the following:
? Please select from one of the below mentioned services: GraphQL
? Provide API name: todoapiamplify
? Choose the default authorization type for the API: Amazon Cognito User Pool
? Do you want to configure advanced settings for the GraphQL API: No
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields

Step 2: Update Schema

Replace the contents of amplify/backend/api/todoapiamplify/schema.graphql:

type Todo @model @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String!
  description: String
  isComplete: Boolean!
  createdAt: AWSDateTime
}

Step 3: Create Todo Component

// src/components/TodoList.js
import React, { useState, useEffect } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { createTodo, deleteTodo, updateTodo } from '../graphql/mutations';
import { listTodos } from '../graphql/queries';
import { onCreateTodo, onDeleteTodo, onUpdateTodo } from '../graphql/subscriptions';
import {
  List, ListItem, ListItemText, ListItemSecondaryAction,
  IconButton, TextField, Button, Checkbox
} from '@mui/material';
import { Delete, Edit } from '@mui/icons-material';

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [formData, setFormData] = useState({ name: '', description: '' });

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

  async function fetchTodos() {
    try {
      const todoData = await API.graphql(graphqlOperation(listTodos));
      setTodos(todoData.data.listTodos.items);
    } catch (err) {
      console.log('error fetching todos:', err);
    }
  }

  async function addTodo(event) {
    event.preventDefault();
    try {
      await API.graphql(graphqlOperation(createTodo, {
        input: {
          name: formData.name,
          description: formData.description,
          isComplete: false
        }
      }));
      setFormData({ name: '', description: '' });
    } catch (err) {
      console.log('error creating todo:', err);
    }
  }

  async function toggleTodoComplete(todo) {
    try {
      await API.graphql(graphqlOperation(updateTodo, {
        input: {
          id: todo.id,
          isComplete: !todo.isComplete
        }
      }));
    } catch (err) {
      console.log('error updating todo:', err);
    }
  }

  async function removeTodo(id) {
    try {
      await API.graphql(graphqlOperation(deleteTodo, { input: { id } }));
    } catch (err) {
      console.log('error deleting todo:', err);
    }
  }

  function subscribeToNewTodos() {
    API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: (todoData) => {
        const newTodo = todoData.value.data.onCreateTodo;
        setTodos(prevTodos => [...prevTodos, newTodo]);
      }
    });
  }

  function subscribeToDeletedTodos() {
    API.graphql(graphqlOperation(onDeleteTodo)).subscribe({
      next: (todoData) => {
        const deletedTodo = todoData.value.data.onDeleteTodo;
        setTodos(prevTodos => prevTodos.filter(todo => todo.id !== deletedTodo.id));
      }
    });
  }

  function subscribeToUpdatedTodos() {
    API.graphql(graphqlOperation(onUpdateTodo)).subscribe({
      next: (todoData) => {
        const updatedTodo = todoData.value.data.onUpdateTodo;
        setTodos(prevTodos => prevTodos.map(todo => 
          todo.id === updatedTodo.id ? updatedTodo : todo
        ));
      }
    });
  }

  return (
    <div>
      <form onSubmit={addTodo}>
        <TextField
          label="Todo Name"
          value={formData.name}
          onChange={e => setFormData({ ...formData, name: e.target.value })}
        />
        <TextField
          label="Description"
          value={formData.description}
          onChange={e => setFormData({ ...formData, description: e.target.value })}
        />
        <Button type="submit" variant="contained" color="primary">
          Add Todo
        </Button>
      </form>

      <List>
        {todos.map(todo => (
          <ListItem key={todo.id}>
            <Checkbox
              checked={todo.isComplete}
              onChange={() => toggleTodoComplete(todo)}
            />
            <ListItemText
              primary={todo.name}
              secondary={todo.description}
            />
            <ListItemSecondaryAction>
              <IconButton edge="end" onClick={() => removeTodo(todo.id)}>
                <Delete />
              </IconButton>
            </ListItemSecondaryAction>
          </ListItem>
        ))}
      </List>
    </div>
  );
}

export default TodoList;

Step 4: Update App Component

// src/App.js
import { Amplify } from 'aws-amplify';
import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { Container, AppBar, Toolbar, Typography, Button } from '@mui/material';
import TodoList from './components/TodoList';
import awsExports from './aws-exports';

Amplify.configure(awsExports);

function App({ signOut, user }) {
  return (
    <div>
      <AppBar position="static">
        <Toolbar>
          <Typography variant="h6" style={{ flexGrow: 1 }}>
            Todo App - Welcome {user.username}
          </Typography>
          <Button color="inherit" onClick={signOut}>
            Sign Out
          </Button>
        </Toolbar>
      </AppBar>
      <Container maxWidth="md" style={{ marginTop: '2rem' }}>
        <TodoList />
      </Container>
    </div>
  );
}

export default withAuthenticator(App);

Deployment

Step 1: Push Changes to Cloud

amplify push

# Confirm all the changes: Yes
# Generate code for API: Yes

Step 2: Deploy Frontend

amplify add hosting

# Select:
? Select the plugin module to execute: Hosting with Amplify Console
? Choose a type: Manual deployment

Then deploy:

amplify publish

Testing the Application

  1. Visit the URL provided after deployment
  2. Create an account and sign in
  3. Try creating, completing, and deleting todos
  4. Verify real-time updates work across different browsers

Common Issues and Solutions

1. API Errors

# If you see GraphQL errors, try:
amplify api gql-compile
amplify push

2. Authentication Issues

# Reset authentication service:
amplify remove auth
amplify add auth
amplify push

3. Deployment Issues

# Clear build files and redeploy:
rm -rf node_modules
rm -rf build
npm install
amplify push
amplify publish

Next Steps

  1. Add Features

    • Due dates for todos
    • Categories/tags
    • Priority levels
    • File attachments
  2. Enhance Security

    • Add MFA
    • Implement social sign-in
    • Add API key for public access
  3. Improve Performance

    • Implement pagination
    • Add caching
    • Optimize images

Resources

Remember to clean up resources when you’re done experimenting:

amplify delete

This will remove all cloud resources associated with your project to avoid unexpected charges.

Conclusion

You’ve now built and deployed a full-stack application using AWS Amplify. This foundation can be extended to build more complex applications with additional features like file storage, analytics, or AI/ML capabilities.

The key concepts covered in this tutorial:

  • Amplify CLI usage
  • Authentication setup
  • API and database configuration
  • Real-time subscriptions
  • Deployment process

Keep exploring the AWS Amplify ecosystem to discover more features and capabilities for your next project!

Published Nov 10, 2024

Welcome to Vians Tech