Todo App
A full-stack todo application demonstrating shared types, server CRUD operations, client reactivity, RPC calls, and component composition.
Full Code
Create todo.tova:
shared {
type Todo {
id: Int
title: String
completed: Bool
}
}
server {
var todos = []
var next_id = 1
fn list_todos() -> [Todo] {
todos
}
fn add_todo(title) -> Todo {
todo = Todo(next_id, title, false)
next_id = next_id + 1
todos = [...todos, todo]
todo
}
fn toggle_todo(id) -> Todo {
todos = todos |> map(fn(t) {
match t.id == id {
true => Todo(t.id, t.title, not t.completed)
false => t
}
})
todos |> filter(fn(t) t.id == id) |> first()
}
fn delete_todo(id) -> Bool {
todos = todos |> filter(fn(t) t.id != id)
true
}
route GET "/api/todos" => list_todos
route POST "/api/todos" => add_todo
route PUT "/api/todos/:id/toggle" => toggle_todo
route DELETE "/api/todos/:id" => delete_todo
}
browser {
state todos = []
state new_title = ""
computed remaining = todos |> filter(fn(t) not t.completed) |> len()
computed total = len(todos)
computed summary = match remaining {
0 => "All done!"
1 => "1 task remaining"
n => "{n} tasks remaining"
}
// Load todos on mount
effect {
result = server.list_todos()
todos = result
}
fn handle_add() {
guard new_title != "" else { return }
todo = server.add_todo(new_title)
todos = [...todos, todo]
new_title = ""
}
fn handle_toggle(id) {
updated = server.toggle_todo(id)
todos = todos |> map(fn(t) {
match t.id == id {
true => updated
false => t
}
})
}
fn handle_delete(id) {
server.delete_todo(id)
todos = todos |> filter(fn(t) t.id != id)
}
component TodoItem(todo) {
<li class={match todo.completed { true => "done" _ => "" }}>
<span class="todo-content" onclick={fn() handle_toggle(todo.id)}>
<span class="check">{match todo.completed { true => "x" _ => "o" }}</span>
<span class="title">{todo.title}</span>
</span>
<button class="delete-btn" onclick={fn() handle_delete(todo.id)}>
"x"
</button>
</li>
}
component App {
<div class="app">
<header>
<h1>"Todos"</h1>
<p class="subtitle">{summary}</p>
</header>
<div class="input-row">
<input
type="text"
placeholder="What needs to be done?"
value={new_title}
oninput={fn(e) new_title = e.target.value}
onkeydown={fn(e) {
match e.key {
"Enter" => handle_add()
_ => nil
}
}}
/>
<button class="btn-add" onclick={fn() handle_add()}>"Add"</button>
</div>
<ul class="task-list">
{todos |> map(fn(todo) TodoItem(todo))}
</ul>
<div class="stats">
"{remaining} of {total} remaining"
</div>
</div>
}
}Run it:
tova dev .Walkthrough
Shared Types
shared {
type Todo {
id: Int
title: String
completed: Bool
}
}The shared block defines types available to both server and client. The Todo type is a record with three fields. Types defined in shared ensure the server and client agree on data shapes at compile time.
Server Block
server {
var todos = []
var next_id = 1
fn add_todo(title) -> Todo {
todo = Todo(next_id, title, false)
next_id = next_id + 1
todos = [...todos, todo]
todo
}
route POST "/api/todos" => add_todo
}Key concepts:
vardeclares mutable server state that persists across requests- Functions contain the business logic.
add_todocreates a newTodo, appends it to the list, and returns it routemaps HTTP methods and paths to handler functions. Parameters in the URL (like:id) are extracted and passed to the handler
RPC Calls
// In the client:
todo = server.add_todo(new_title)The server. prefix generates an RPC call to the corresponding server function. Tova compiles this to a fetch() call to the appropriate route, serializing arguments and deserializing the response. Shared types guarantee the data format matches.
Client Reactivity
browser {
state todos = []
state new_title = ""
computed remaining = todos |> filter(fn(t) not t.completed) |> len()
}statecreates reactive variables. Assigning a new value triggers reactive updates.computedderives values from state.remainingautomatically recalculates whenevertodoschanges.
Effects
effect {
result = server.list_todos()
todos = result
}An effect block runs after the component mounts. This is where you perform side effects like fetching data from the server. Here, the todos are loaded from the server and stored in the reactive todos state.
Guard Clauses
fn handle_add() {
guard new_title != "" else { return }
todo = server.add_todo(new_title)
todos = [...todos, todo]
new_title = ""
}guard provides early return when a condition is not met. If new_title is empty, the function returns without doing anything.
Component Composition
component TodoItem(todo) {
<li class={match todo.completed { true => "done" _ => "" }}>
<span onclick={fn() handle_toggle(todo.id)}>{todo.title}</span>
<button onclick={fn() handle_delete(todo.id)}>"x"</button>
</li>
}
component App {
<ul>
{todos |> map(fn(todo) TodoItem(todo))}
</ul>
}Components can accept parameters and be composed together. The App component maps over the todos list, rendering a TodoItem for each entry. When todos changes, only the affected list items update.
Inline Match in JSX
<li class={match todo.completed { true => "done" _ => "" }}>Match expressions can be used inline within JSX attributes. This sets the CSS class based on the todo's completion status.
What's Next
- Add real-time updates with the Chat App example
- Split into multiple servers with Multi-Server Architecture
- Add authentication with Auth Flow