How-to: a todo app

A complete todo app: a static page that lists, adds and toggles todos, backed by a real API — all from one poops.json, served on one origin by laxative.

1. Scaffold

mkdir todo && cd todo && npm init -y
npm i -D poops laxative

2. One config

poops.json — the frontend (poops), the backend (septic), and the generated form, together:

{
  "markup": { "in": "src/markup", "out": "dist" },
  "septic": {
    "db": "data/todos.db",
    "resources": {
      "todos": {
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "access": { "read": "public", "write": "public" },
        "fields": {
          "title":   "string required",
          "done":    "boolean = false",
          "created": "datetime = now",
          "updated": "datetime = now!"
        }
      }
    },
    "build": {
      "forms": { "todos": { "into": "src/markup/_partials", "submitLabel": "Add" } }
    }
  }
}

That's the entire backend: a todos table, /api/todos CRUD, and a generated add-todo form.

3. The page

src/markup/index.html. Because laxative serves the API and the site on one origin, the page just talks to /api/todos — no CORS, no proxy. Here with Alpine for the interactive list and toggle:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8"><title>Todos</title>
  <script src="https://unpkg.com/alpinejs" defer></script>
</head>
<body x-data="todos()" x-init="load()">
  <h1>Todos</h1>

  <form @submit.prevent="add()">
    <input x-model="title" placeholder="What needs doing?" required>
    <button>Add</button>
  </form>

  <ul>
    <template x-for="t in items" :key="t.id">
      <li>
        <input type="checkbox" :checked="t.done" @change="toggle(t)">
        <span x-text="t.title" :style="t.done && 'text-decoration:line-through'"></span>
      </li>
    </template>
  </ul>

  <script>
    function todos() {
      return {
        items: [], title: '',
        async load() { this.items = await (await fetch('/api/todos?sort=created&order=desc')).json() },
        async add() {
          await fetch('/api/todos', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: this.title }) })
          this.title = ''; this.load()
        },
        async toggle(t) {
          await fetch('/api/todos/' + t.id, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ done: !t.done }) })
          this.load()
        }
      }
    }
  </script>
</body>
</html>

Prefer no JavaScript? septic build also emits src/markup/_partials/todos-form.html — a real <form> that POSTs to /api/todos and works without a line of client code. Include it with {% include "todos-form.html" %} (add _partials to includePaths). See septic's how-to.

4. Run it

npx laxative dev      # http://localhost:3000 — page + /api, one origin

Add a todo, tick it off — every request hits /api/todos on the same host that served the page. That's the whole point of laxative.

5. Ship it

npx laxative build    # dist/ (static site + generated form) + the DB
npx laxative serve    # one process: dist/ + /api + /uploads

One poops.json → a working full-stack todo app. Define a resource, write a page, done.