Changing and deleting todos
Page 7 of 14 in Learn Jadpo
By the end you can mark a todo done, edit it and delete it.
This page adds the routes that change a todo: PATCH / edits
its title or marks it done, and DELETE / removes it.
What a change may hold
A PATCH sends only the fields it changes. So every field of the input
is optional:
domains/:
use todos/types { TodoStatus, TodoTitle }/// What a caller may change about a todo: any of these, or none.type TodoChanges = Input { title: TodoTitle { optional: true } status: TodoStatus { optional: true }}
A field the caller leaves out isn't changed. So {"status": "done"}
marks a todo done and leaves its title as it was.
Load, then update
Changing a record is two steps in one action: load it, then update it.
domains/:
use todos/entities { Todo, TodoNotFound }use todos/types { TodoChanges, TodoView }/// Changes the fields of a todo that the caller sent.action change_todo(todo_id: Todo.id, changes: TodoChanges) fails TodoNotFound -> TodoView { var todo: Todo = attempt Todo.get(todo_id) var changed: Todo = Todo.update(todo) { patch: changes } return TodoView { id: changed.id title: changed.title status: changed.status due_at: changed.due_at created_at: changed.created_at }}
Todo.get(todo_id)loads one todo, or fails withTodoNotFound, like the query on page 6. Every entity getsget, as it getscreate.attemptin front of a call that can fail passes its failure on: ifTodo.getfails,change_todostops with the same failure. That's whychange_todosaysfails TodoNotFound. Withoutattempt, the compiler refuses the call, so a failure can't be ignored by accident.Todo.update(todo) { patch: changes }changes only the fields the caller sent, and gives back the todo as stored. To set fields from code instead, writeset: { status: TodoStatus.done }.
jadpo sql change_todo shows how a patch reaches the database:
write Todo.update UPDATE "todo" SET "title" = CASE WHEN $1 THEN $2 ELSE "title" END, "status" = CASE WHEN $3 THEN $4 ELSE "status" END WHERE "id" = $5 AND ($6::text IS NULL OR "xmin"::text = $6) RETURNING "id", "title", "status", "due_at", "created_at" writes: updates the Todo loaded earlier file: domains/todos/actions/change_todo.jadpo:7 sends: 1 statement $1: whether changes supplies title · boolean $2: changes.title · text $3: whether changes supplies status · boolean $4: changes.status · text $5: todo.id · uuid $6: NULL::text · text
The CASE lines change a column only when the caller sent that field.
The last condition compares PostgreSQL's row version, xmin. Jadpo
fills it in only when an action calls another service between loading a
record and changing it, to catch a change someone made meanwhile. Here
$6 is NULL, so it does nothing.
Deleting is the same shape: load, then delete.
domains/:
use todos/entities { Todo, TodoNotFound }/// Deletes a todo.action delete_todo(todo_id: Todo.id) fails TodoNotFound -> Unit { var todo: Todo = attempt Todo.get(todo_id) Todo.delete(todo)}
Unit is the result of an action that gives nothing back, like void.
All or nothing
Each action runs in one database transaction. If it fails partway, with
a reject or a failure passed on by attempt, everything it wrote is
rolled back. You never open or commit a transaction yourself. The
reference also covers what happens when two requests change the same
todo at once.
The routes
domains/, changed. The PATCH and DELETE routes and the names they add to the use lines are new.
use todos/entities { Todo }use todos/types { NewTodo, TodoChanges, TodoFilter, TodoView }use todos/actions { add_todo, change_todo, delete_todo }use todos/queries { list_todos, todo_by_id }/// The todo list.routes Todos { prefix: /todos /// Adds a todo. POST / { authentication: RouteAuthentication.none input: NewTodo output: TodoView status: SuccessStatus.created run: add_todo(input) } /// Lists todos, newest first. GET / { authentication: RouteAuthentication.none input: TodoFilter output: List<TodoView> run: list_todos(input) } /// One todo. GET /{todo_id: Todo.id} { authentication: RouteAuthentication.none output: TodoView run: todo_by_id(todo_id) } /// Changes a todo's title or status. PATCH /{todo_id: Todo.id} { authentication: RouteAuthentication.none input: TodoChanges output: TodoView run: change_todo(todo_id, input) } /// Deletes a todo. DELETE /{todo_id: Todo.id} { authentication: RouteAuthentication.none status: SuccessStatus.no_content run: delete_todo(todo_id) }}
SuccessStatus.no_content answers 204 with no body, as a delete
usually does.
The entity has to allow the changes too. Add update: and delete: to
its policy:
domains/, changed. The update: and delete: lines are new.
use todos/types { TodoStatus, TodoTitle }/// Something to do, kept in the database.entity Todo { id: Uuid title: TodoTitle status: TodoStatus due_at: Instant? created_at: Instant { generated: Generated.on_create } policy: { create: [Access.public] read: [Access.public] update: [Access.public] delete: [Access.public] }}
Update the record, and jadpo check passes.
Try it
Run jadpo dev, add a todo as on page 4, and use its id. Mark it done:
curl -X PATCH http://localhost:3000/todos/01a18991-3d34-78a5-b36d-e043061c61a0 \ -H 'content-type: application/json' \ -d '{"status": "done"}'
{"id":"01a18991-3d34-78a5-b36d-e043061c61a0","title":"Buy milk","status":"done","due_at":null,"created_at":"2026-10-29T21:29:19.412Z"}Change its title, leaving the status alone:
curl -X PATCH http://localhost:3000/todos/01a18991-3d34-78a5-b36d-e043061c61a0 \ -H 'content-type: application/json' \ -d '{"title": "Buy oat milk"}'
{"id":"01a18991-3d34-78a5-b36d-e043061c61a0","title":"Buy oat milk","status":"done","due_at":null,"created_at":"2026-10-29T21:29:19.412Z"}Delete it, then try again:
curl -i -X DELETE http://localhost:3000/todos/01a18991-3d34-78a5-b36d-e043061c61a0curl -X DELETE http://localhost:3000/todos/01a18991-3d34-78a5-b36d-e043061c61a0
HTTP/1.1 204 No Contentx-request-id: req_2c14f1f910abf38fdate: Thu, 29 Oct 2026 21:29:19 GMT{"error":{"code":"todo_not_found","message":"Not found.","request_id":"req_be2371df39b011e2"}}
What you have
A todo API you can add to, read, change and delete from. You've checked
each route by hand with curl. Next, on page 8, you write tests that do
it for you, each against a fresh database.
Read more in the reference: The operations every entity gets and All-or-nothing actions and concurrent changes.
Markdown for agents: /learn/learn-changing.md.Built from Jadpo main at a707efb. Every file on this page is compiled and tested by Jadpo's CI, on the app as the pages before it built it.