HomeView now shows a "You have N lists." summary above the list, moves the new-list form beneath the list, and drops the description input (title only). lists store createList() loses its description parameter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PHP Todo List
A small todo-list application: a REST API written in PHP (Slim 4) backed by an SQLite file, plus a Vue 3 + TypeScript PWA frontend in web/.
Status
| Stage | Scope | State |
|---|---|---|
| 1 | Auth API — register, login, GET /me |
✅ done |
| 2 | Frontend shell — Vite PWA, auth-gated routing, register/login pages | ✅ done |
| 3 | Todo list + item CRUD API | ✅ done |
| 4 | Frontend lists view — list index + create form | ✅ done |
| 5 | Frontend list detail — items UI | planned |
Registration signs the user in immediately, with the account's email marked
unverified (user.email_verified is false until a future stage adds a
verification endpoint).
Run with Docker
The only requirement is Docker with the Compose plugin.
docker compose up -d
This builds a PHP 8.3 + Apache image, applies migrations, and serves the API at
http://localhost:8080 (e.g. curl http://localhost:8080/api/health).
- The project directory is bind-mounted into the container, so editing PHP
source takes effect without a rebuild (within ~2s, due to the opcache
revalidation interval).
vendor/is used from the host — runcomposeronce first if it is missing (see "Run without Docker" below, ordocker compose run --rm --entrypoint composer app install). - The SQLite database and the generated JWT signing key live in the
storagenamed volume, mounted at/var/www/storage(outside the bind-mounted source), so they survivedocker compose restart/down+up. - Rebuild only after changing the
Dockerfile:docker compose up -d --build. docker compose down -vremoves the volume and gives you a clean database.- Override settings via the environment or a
.envfile in this directory (Compose substitutesAPP_DEBUG,JWT_SECRET,JWT_TTL— see docker-compose.yml).
Run without Docker
Requires PHP 8.1+ with the pdo_sqlite and mbstring extensions, plus
Composer. On Fedora:
sudo dnf install php-cli php-pdo php-mbstring composer
Setup
composer install
cp .env.example .env # optional; sane defaults are used without it
composer migrate # creates storage/database.sqlite and its tables
Running
composer serve # http://localhost:8080 (php -S localhost:8080 -t public)
Any web server can serve the app as long as the document root is public/ and
unknown paths fall through to public/index.php.
Frontend
The Vue/TypeScript PWA lives in web/ and talks to this API. With the API
running (docker compose up -d):
cd web
npm install
npm run dev # http://localhost:5173, proxies /api to localhost:8080
Or run it inside Compose alongside the API:
docker compose --profile frontend up -d
Unauthenticated visitors are redirected to /login; /register creates an
account and signs in immediately. See web/README.md.
Configuration
All settings are optional environment variables (read from .env or the real
environment). See .env.example.
| Variable | Default | Purpose |
|---|---|---|
APP_DEBUG |
false |
Include exception details in error responses |
DATABASE_PATH |
storage/database.sqlite |
SQLite file location |
JWT_SECRET |
auto-generated into storage/secret.key |
Token signing key |
JWT_TTL |
86400 |
Token lifetime in seconds |
API
Base path: /api. All request and response bodies are JSON; send
Content-Type: application/json.
GET /api/health
{ "status": "ok" }
POST /api/auth/register
Request:
{ "email": "ada@example.com", "password": "correct horse battery staple" }
201 Created:
{
"user": {
"id": 1,
"email": "ada@example.com",
"email_verified": false,
"email_verified_at": null,
"created_at": "2026-09-03T12:00:00Z"
},
"token": "<jwt>",
"expires_at": "2026-09-04T12:00:00+00:00"
}
New accounts are created with an unverified email (email_verified: false).
Errors: 422 invalid input, 409 email already registered.
Validation: email must be a valid address (≤ 255 chars); password must be
8–72 characters.
POST /api/auth/login
Request:
{ "email": "ada@example.com", "password": "correct horse battery staple" }
200 OK: same shape as register. 401 on bad credentials (the message does not
say whether it was the email or the password that was wrong).
GET /api/me
Requires Authorization: Bearer <jwt>.
200 OK:
{
"user": {
"id": 1,
"email": "ada@example.com",
"email_verified": false,
"email_verified_at": null,
"created_at": "2026-09-03T12:00:00Z"
}
}
401 if the header is missing, malformed, or the token is invalid/expired.
Todo lists
All routes below require Authorization: Bearer <jwt>. A list belongs to one
owner (the creator); another user's list — or a missing one — always responds
404.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/lists |
the caller's lists, sorted A→Z by title |
POST |
/api/lists |
create a list |
GET |
/api/lists/{id} |
one list |
PATCH |
/api/lists/{id} |
update title and/or description |
DELETE |
/api/lists/{id} |
delete the list and its items (204) |
GET /api/lists is always ordered alphabetically (case-insensitive) by title;
there is no other sort option. A user may own at most 100 lists — creating
one beyond that responds 409.
Create/update body: title (required on create, 1–255 chars), description
(optional, ≤ 2000 chars, defaults to ""). PATCH needs at least one field.
List representation:
{
"list": {
"id": 1,
"title": "Shopping",
"description": "For the week",
"owner_id": 1,
"item_count": 3,
"completed_count": 1,
"created_at": "2026-09-03T12:00:00Z",
"updated_at": "2026-09-03T12:00:00Z"
}
}
GET /api/lists returns { "lists": [ … ] }.
Todo items
Scoped to a list; the parent list's ownership is checked first (404 otherwise).
| Method | Path | Purpose |
|---|---|---|
GET |
/api/lists/{id}/items |
items, ordered by position then id |
POST |
/api/lists/{id}/items |
add an item |
GET |
/api/lists/{id}/items/{itemId} |
one item |
PATCH |
/api/lists/{id}/items/{itemId} |
update text, complete, and/or position |
DELETE |
/api/lists/{id}/items/{itemId} |
delete the item (204) |
Create body: text (required, 1–1000 chars), complete (optional bool,
default false), position (optional integer ≥ 0; when omitted the item is
appended after the current highest position). PATCH needs at least one field.
position is a plain sort key the client manages — updating one item never
renumbers its siblings.
Item representation:
{
"item": {
"id": 10,
"list_id": 1,
"text": "Milk",
"complete": false,
"position": 0,
"created_at": "2026-09-03T12:00:00Z",
"updated_at": "2026-09-03T12:00:00Z"
}
}
GET …/items returns { "items": [ … ] }.
Error shape
Every error response looks like:
{ "error": { "message": "The submitted data was invalid.", "details": { "email": ["Email must be a valid address."] } } }
details is present only when relevant (e.g. validation).
Try it
BASE=http://localhost:8080
curl -s -X POST $BASE/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}'
TOKEN=$(curl -s -X POST $BASE/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}' | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
curl -s $BASE/api/me -H "Authorization: Bearer $TOKEN"
LIST=$(curl -s -X POST $BASE/api/lists \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Shopping","description":"For the week"}' \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
curl -s -X POST $BASE/api/lists/$LIST/items \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"Milk"}'
curl -s $BASE/api/lists/$LIST/items -H "Authorization: Bearer $TOKEN"
Tests
composer install # installs phpunit (require-dev)
vendor/bin/phpunit
Layout
public/index.php Front controller
src/bootstrap.php App wiring and route definitions
src/Support/Config.php Environment-driven configuration
src/Support/Database.php PDO/SQLite connection
src/Auth/JwtService.php Issue/verify JWTs
src/Auth/AuthMiddleware.php Bearer-token authentication
src/Http/JsonErrorHandler.php Uniform JSON error envelope
src/Http/Controllers/ Request handlers (Auth, TodoList, TodoItem)
src/Repository/ Database access (User, TodoList, TodoItem)
src/Support/Validator.php Request-body validation helper
migrations/*.sql Schema, applied by bin/migrate.php
Dockerfile PHP 8.3 + Apache image
docker-compose.yml One-command local stack (API; web via --profile frontend)
docker/ Apache vhost + container entrypoint
web/ Vue 3 + TypeScript + Vite PWA frontend
Provenance
This project was generated with Claude Code.