← All projects

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

embed
let 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();