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.