Node.js · Vercel Functions · JavaScript · REST API
Todo List API
A task manager with full CRUD backed by a real REST API — add, complete, and delete tasks with every action hitting the server.
Tasks are stored and mutated entirely server-side through a small REST API (GET/POST/PATCH/DELETE on /api/todos). The frontend is plain HTML/CSS/JS with no framework and no build step — every checkbox and delete button fires a real HTTP request.
Live demo
embedlet todos = [
{ id: '1', title: 'Write the project README', done: true },
{ id: '2', title: 'Wire up the API', done: false },
{ id: '3', title: 'Ship the demo', done: false },
];
function nextId() {
return (Math.max(0, ...todos.map((t) => Number(t.id) || 0)) + 1).toString();
}
module.exports = (req, res) => {
const { method } = req;
if (method === 'GET') {
res.status(200).json(todos);
return;
}
if (method === 'POST') {
const title = (req.body && req.body.title || '').toString().trim();
if (!title) {
res.status(400).json({ error: 'title is required' });
return;
}
const todo = { id: nextId(), title, done: false };
todos.push(todo);
res.status(201).json(todo);
return;
}
if (method === 'PATCH') {
const id = (req.query.id || '').toString();
const todo = todos.find((t) => t.id === id);
if (!todo) {
res.status(404).json({ error: 'not found' });
return;
}
if (req.body && typeof req.body.title === 'string' && req.body.title.trim()) {
todo.title = req.body.title.trim();
}
if (req.body && typeof req.body.done === 'boolean') {
todo.done = req.body.done;
} else {
todo.done = !todo.done;
}
res.status(200).json(todo);
return;
}
if (method === 'DELETE') {
const id = (req.query.id || '').toString();
const before = todos.length;
todos = todos.filter((t) => t.id !== id);
if (todos.length === before) {
res.status(404).json({ error: 'not found' });
return;
}
res.status(204).end();
return;
}
res.status(405).json({ error: 'method not allowed' });
};
const list = document.getElementById('list');
const form = document.getElementById('form');
const titleInput = document.getElementById('title');
async function load() {
const res = await fetch('/api/todos');
const todos = await res.json();
render(todos);
}
function render(todos) {
list.innerHTML = '';
if (!todos.length) {
list.innerHTML = '<p class="empty">No tasks yet. Add one above.</p>';
return;
}
for (const todo of todos) {
const item = document.createElement('div');
item.className = 'item' + (todo.done ? ' done' : '');
item.innerHTML = `
<input type="checkbox" ${todo.done ? 'checked' : ''}>
<span class="title"></span>
<button class="icon" title="Delete">✕</button>
`;
item.querySelector('span.title').textContent = todo.title;
item.querySelector('input').addEventListener('change', () => toggle(todo.id));
item.querySelector('button').addEventListener('click', () => remove(todo.id));
list.appendChild(item);
}
}
async function toggle(id) {
await fetch(`/api/todos?id=${encodeURIComponent(id)}`, { method: 'PATCH' });
load();
}
async function remove(id) {
await fetch(`/api/todos?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
load();
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const title = titleInput.value.trim();
if (!title) return;
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
titleInput.value = '';
load();
});
load();