Back to Tutorials

Featured Building a Cloud-Based Todo App with REST APIs

Overview

In this tutorial, we'll build a complete todo application that uses REST APIs to communicate with a cloud backend. This will demonstrate how to integrate APIs into a real-world application.

Architecture

Our app will have three main components:

  • Frontend: HTML, CSS, JavaScript (or React/Vue)
  • Backend API: Node.js/Express server
  • Database: Cloud database (MongoDB Atlas or PostgreSQL)

Step 1: Setting Up the Backend API

Create your Express server with todo endpoints:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(express.json());

// Connect to MongoDB Atlas (cloud database)
mongoose.connect('mongodb+srv://username:password@cluster.mongodb.net/todos', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Todo Schema
const todoSchema = new mongoose.Schema({
  title: String,
  completed: Boolean,
  createdAt: { type: Date, default: Date.now }
});

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

// GET all todos
app.get('/api/todos', async (req, res) => {
  try {
    const todos = await Todo.find();
    res.json(todos);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// POST create todo
app.post('/api/todos', async (req, res) => {
  try {
    const todo = new Todo({
      title: req.body.title,
      completed: false
    });
    await todo.save();
    res.status(201).json(todo);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// PUT update todo
app.put('/api/todos/:id', async (req, res) => {
  try {
    const todo = await Todo.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true }
    );
    if (!todo) {
      return res.status(404).json({ error: 'Todo not found' });
    }
    res.json(todo);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// DELETE todo
app.delete('/api/todos/:id', async (req, res) => {
  try {
    const todo = await Todo.findByIdAndDelete(req.params.id);
    if (!todo) {
      return res.status(404).json({ error: 'Todo not found' });
    }
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Step 2: Creating the Frontend

Create an HTML file with JavaScript to interact with your API:

<!DOCTYPE html>
<html>
<head>
  <title>Todo App</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; }
    input { padding: 10px; width: 70%; }
    button { padding: 10px 20px; }
    .todo-item { padding: 10px; margin: 5px 0; background: #f0f0f0; }
    .completed { text-decoration: line-through; opacity: 0.6; }
  </style>
</head>
<body>
  <h1>My Todo App</h1>
  <div>
    <input type="text" id="todoInput" placeholder="Add a new todo...">
    <button onclick="addTodo()">Add</button>
  </div>
  <div id="todos"></div>

  <script>
    const API_URL = 'http://localhost:3000/api/todos';

    // Load todos on page load
    async function loadTodos() {
      try {
        const response = await fetch(API_URL);
        const todos = await response.json();
        displayTodos(todos);
      } catch (error) {
        console.error('Error loading todos:', error);
      }
    }

    // Display todos
    function displayTodos(todos) {
      const container = document.getElementById('todos');
      container.innerHTML = todos.map(todo => `
        <div class="todo-item ${todo.completed ? 'completed' : ''}">
          <input type="checkbox" ${todo.completed ? 'checked' : ''} 
                 onchange="toggleTodo('${todo._id}')">
          <span>${todo.title}</span>
          <button onclick="deleteTodo('${todo._id}')">Delete</button>
        </div>
      `).join('');
    }

    // Add new todo
    async function addTodo() {
      const input = document.getElementById('todoInput');
      const title = input.value.trim();
      if (!title) return;

      try {
        const response = await fetch(API_URL, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ title })
        });
        const todo = await response.json();
        input.value = '';
        loadTodos();
      } catch (error) {
        console.error('Error adding todo:', error);
      }
    }

    // Toggle todo completion
    async function toggleTodo(id) {
      try {
        const todo = document.querySelector(`[onchange*="${id}"]`).closest('.todo-item');
        const completed = todo.querySelector('input[type="checkbox"]').checked;
        
        await fetch(`${API_URL}/${id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ completed })
        });
        loadTodos();
      } catch (error) {
        console.error('Error updating todo:', error);
      }
    }

    // Delete todo
    async function deleteTodo(id) {
      try {
        await fetch(`${API_URL}/${id}`, { method: 'DELETE' });
        loadTodos();
      } catch (error) {
        console.error('Error deleting todo:', error);
      }
    }

    // Load todos when page loads
    loadTodos();
  </script>
</body>
</html>

Step 3: Deploying to the Cloud

Deploy your backend to a cloud platform:

  • Heroku: Easy deployment with Git
  • AWS Elastic Beanstalk: Scalable AWS deployment
  • Google Cloud Run: Serverless container deployment
  • Vercel/Netlify: Great for frontend deployment

Key Concepts

  • API Endpoints: URLs that your app calls to get/send data
  • HTTP Methods: GET (read), POST (create), PUT (update), DELETE (remove)
  • JSON: Data format used for API communication
  • Async/Await: Handle API calls asynchronously

Conclusion

You now have a fully functional cloud-based todo app! This pattern can be applied to build any application that needs to store and retrieve data from the cloud.