Full-Stack Demo: Todo API

Complete REST-style Todo API over an in-memory store

/#
## Todo API Service

A REST-style API for todo management.
Demonstrates a more complete service pattern.
#/

// Simulated persistence layer
const todos = new Map()
let nextId = 1

// Types are inferred from function signatures below
// Todo: { id: 0, title: '', completed: false, createdAt: '' }
// CreateInput: { title: 'Buy milk' }
// UpdateInput: { id: 1, title: 'Buy milk', completed: false }

// POST /todos - Create
export function createTodo(input: { title: 'New todo' }):! { id: 0, title: '', completed: false, createdAt: '' } {
  const todo = {
    id: nextId++,
    title: input.title,
    completed: false,
    // Raw `Date` is not allowed in .tjs; LegacyDate is the named escape.
    createdAt: LegacyDate().toISOString()
  }
  todos.set(todo.id, todo)
  return todo
}

// GET /todos/:id - Read one (null if not found)
export function getTodo(input: { id: 1 }):! { id: 0, title: '', completed: false, createdAt: '' } | null {
  return todos.get(input.id) ?? null
}

// GET /todos - Read all, or filter with { completed: true | false }.
// A bare `null` default accepts any value, so the filter is optional AND free-form.
export function listTodos(filter = null):! { todos: [{ id: 0, title: '', completed: false, createdAt: '' }] } {
  let items = [...todos.values()]

  if (filter?.completed !== undefined) {
    items = items.filter(t => t.completed === filter.completed)
  }

  return { todos: items }
}

// PUT /todos/:id - Update (null if not found)
export function updateTodo(input: { id: 1, title: '', completed: false }):! { id: 0, title: '', completed: false, createdAt: '' } | null {
  const existing = todos.get(input.id)
  if (!existing) return null

  const updated = {
    ...existing,
    title: input.title ?? existing.title,
    completed: input.completed ?? existing.completed
  }
  todos.set(input.id, updated)
  return updated
}

// DELETE /todos/:id - Delete
export function deleteTodo(input: { id: 1 }):! { deleted: true } {
  const existed = todos.has(input.id)
  todos.delete(input.id)
  return { deleted: existed }
}

// PATCH /todos/:id/toggle - Toggle completion (null if not found)
export function toggleTodo(input: { id: 1 }):! { id: 0, title: '', completed: false, createdAt: '' } | null {
  const todo = todos.get(input.id)
  if (!todo) return null

  todo.completed = !todo.completed
  return todo
}

// DELETE /todos/completed - Clear completed
export function clearCompleted():! { cleared: 0 } {
  let cleared = 0
  for (const [id, todo] of todos) {
    if (todo.completed) {
      todos.delete(id)
      cleared++
    }
  }
  return { cleared }
}

// Tests
test 'CRUD operations work' {
  const todo = createTodo({ title: 'Test todo' })
  expect(todo.id).toBeGreaterThan(0)
  expect(todo.completed).toBe(false)

  const fetched = getTodo({ id: todo.id })
  expect(fetched.title).toBe('Test todo')

  const toggled = toggleTodo({ id: todo.id })
  expect(toggled.completed).toBe(true)

  const deleted = deleteTodo({ id: todo.id })
  expect(deleted.deleted).toBe(true)
  expect(getTodo({ id: todo.id })).toBe(null)
}

// Demo
console.log('=== Todo API Demo ===\n')

// Create todos
createTodo({ title: 'Learn TJS' })
createTodo({ title: 'Build something cool' })
createTodo({ title: 'Ship it' })

const titles = (list) => list.todos.map(t => t.title).join(', ')

console.log('Created 3 todos')
console.log('All:', titles(listTodos()))

// Complete first one
const first = listTodos().todos[0]
toggleTodo({ id: first.id })
console.log('\nToggled first todo')
console.log('Completed:', titles(listTodos({ completed: true })))
console.log('Pending:', titles(listTodos({ completed: false })))

// Clear completed
console.log('\nClearing completed...')
console.log(clearCompleted())
console.log('Remaining:', titles(listTodos()))

// A request with the wrong shape is refused before the handler runs
const bad = createTodo({ title: 42 })
console.log('\ncreateTodo({ title: 42 }) ->', bad instanceof Error ? bad.message : bad)

What to notice

A gotcha: object defaults merge

Why filter = null and not filter = { completed: false }? In TJS an object-literal default is a dictionary: members are defaulted one by one and merged into whatever the caller passes. That is usually what you want, but it would turn "no filter" into "only pending":

function withDictionary(filter = { completed: false }) {
  return filter
}

function withNull(filter = null) {
  return filter
}

console.log('dictionary, called with {}:', JSON.stringify(withDictionary({})))
console.log('null default, called with {}:', JSON.stringify(withNull({})))

The dictionary filled in completed: false for the caller who asked for everything. When a default should mean "absent" rather than "these values", use null, or keep JavaScript's all-or-nothing default for one parameter with LegacyDefault({ … }).

Limits