Step 8: Backend with Walkers#
** Quick Tip: Each step has two parts. Part 1 shows you what to build. Part 2** explains why it works. Want to just build? Skip all Part 2 sections!
In this step, you'll add a real backend to your app using walkers - so your todos are stored on a server!
Part 1: Building the App#
Step 8.1: Define the Todo Node#
First, let's define our data structure. Add this OUTSIDE the cl { } block (at the top of your file):
# Backend - Data Model
node Todo {
has text: str;
has done: bool = False;
}
# Backend - We'll add walkers here soon
cl import from react {useState, useEffect}
cl {
# ... your frontend code
}
Step 8.2: Create Your First Walker - Read Todos#
Add these walkers AFTER the node definition but BEFORE the cl { block:
# Backend - Data Model
node Todo {
has text: str;
has done: bool = False;
}
# Backend - Walkers
walker read_todos {
can read with `root entry {
visit [-->(`?Todo)];
}
can report_todos with Todo entry {
report here;
}
}
cl import from react {useState, useEffect}
cl {
# ... your frontend code
}
Step 8.3: Create Walker for Adding Todos#
Add this walker:
walker create_todo {
has text: str;
can create with `root entry {
new_todo = here ++> Todo(text=self.text);
report new_todo;
}
}
Step 8.4: Create Walkers for Toggle and Delete#
Add these walkers:
Your complete backend should now look like this:
# Backend - Data Model
node Todo {
has text: str;
has done: bool = False;
}
# Backend - Walkers
walker create_todo {
has text: str;
can create with `root entry {
new_todo = here ++> Todo(text=self.text);
report new_todo;
}
}
walker read_todos {
can read with `root entry {
visit [-->(`?Todo)];
}
can report_todos with Todo entry {
report here;
}
}
walker toggle_todo {
can toggle with Todo entry {
here.done = not here.done;
report here;
}
}
# Frontend (keep all your existing code)
cl import from react {useState, useEffect}
cl {
# ... all your frontend components
}
Step 8.5: Call Walkers from Frontend - Load Todos#
Update your useEffect to load todos from the backend:
def app() -> any {
let [todos, setTodos] = useState([]);
let [input, setInput] = useState("");
let [filter, setFilter] = useState("all");
# Load todos from backend when app mounts
useEffect(lambda -> None {
async def loadTodos() -> None {
result = root spawn read_todos();
setTodos(result.reports if result.reports else []);
}
loadTodos();
}, []);
# ... rest of your code
}
Step 8.6: Call Walkers from Frontend - Add Todo#
Update your addTodo function:
# Add todo
async def addTodo() -> None {
if not input.trim() {
return;
}
# Call backend walker
result = root spawn create_todo(text=input.trim());
# Add the new todo to local state
setTodos(todos.concat([result.reports[0][0]]));
setInput("");
}
Step 8.7: Call Walkers from Frontend - Toggle and Delete#
Update your toggle and delete functions:
# Toggle todo
async def toggleTodo(id: any) -> None {
# Call backend walker
id spawn toggle_todo();
# Update local state
setTodos(todos.map(lambda todo: any -> any {
if todo._jac_id == id {
return {
"_jac_id": todo._jac_id,
"text": todo.text,
"done": not todo.done
};
}
return todo;
}));
}
# Delete todo
async def deleteTodo(id: any) -> None {
# Call backend walker
#id spawn delete_todo();
# Update local state
setTodos(todos.filter(lambda todo: any -> bool {
return todo._jac_id != id;
}));
}
Step 8.8: Update TodoItem to Use _jac_id#
When rendering todos, use _jac_id instead of custom id:
# In your app() function
<div>
{filteredTodos.map(lambda todo: any -> any {
return <TodoItem
key={todo._jac_id}
id={todo._jac_id}
text={todo.text}
done={todo.done}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>;
})}
</div>
Try it! 1. Add some todos 2. Check/uncheck them 3. Delete some 4. Refresh the page - your todos persist!
⏭ Want to skip the theory? Jump to Step 9: Authentication
Part 2: Understanding the Concepts#
What Are Walkers?#
Walkers are backend functions that: - Run on the server (not in the browser) - Can traverse your data graph - Automatically become API endpoints - Are called from your frontend
Traditional way (Flask):
# Backend - separate file
@app.route("/api/todos", methods=["GET"])
def get_todos():
todos = db.query(Todo).all()
return jsonify(todos)
Jac way:
# Backend - same file!
walker read_todos {
can read with `root entry {
visit [-->(`?Todo)];
}
can report_todos with Todo entry {
report here;
}
}
No routes, no manual API setup - it just works!
The spawn Syntax#
This is how you call walkers from your frontend:
# Syntax
node_reference spawn walker_name(parameters);
# Examples
root spawn read_todos(); # On root node
root spawn create_todo(text="New todo"); # With parameters
todoId spawn toggle_todo(); # On specific node
What happens: 1. Request sent to server 2. Walker runs on server 3. Data stored in backend 4. Response sent back to frontend
Graph Structure#
Your data is stored as a graph:
When you call read_todos:
1. Walker starts at root
2. Follows edges (-->) to find Todo nodes
3. Reports each Todo found
Creating Connections (++>)#
Breakdown:
- here - Current node (root)
- ++> - Create node and connect it
- Todo(...) - New node to create
- Result: New Todo connected to root
Visiting Nodes#
Breakdown:
- visit - Traverse to these nodes
- --> - Follow outgoing edges
- `?Todo - Find nodes of type Todo
- [...] - Array of nodes to visit
Reporting Data#
report new_todo; # Report a node
report here; # Report current node
report {"success": True}; # Report an object
report sends data back to the frontend. All reports are collected in the result.reports array.
The _jac_id Field#
Every node gets a unique _jac_id:
Use this ID to reference specific nodes:
Backend vs Frontend Code#
# Backend (runs on server)
node Todo {
has text: str;
}
walker create_todo {
has text: str;
can create with `root entry {
# This code runs on the server
}
}
# Frontend (runs in browser)
cl {
def app() -> any {
# This code runs in the browser
let result = root spawn create_todo(text="Todo");
}
}
Data Persistence#
localStorage (Step 7): - Stored in browser only - Lost when you clear browser data - Not shared across devices
Walkers (Step 8): - Stored on server - Persists forever - Accessible from any device - Per-user (each user sees only their data)
async/await for Walkers#
Walker calls are asynchronous:
# Must use async/await
async def addTodo() -> None {
result = await root spawn create_todo(text="Todo");
# Wait for result before continuing
}
# Or use it in a lambda
useEffect(lambda -> None {
async def loadTodos() -> None {
result = await root spawn read_todos();
setTodos(result.reports);
}
loadTodos();
}, []);
What You've Learned#
- What walkers are (backend functions)
- How to define data models with nodes
- Creating walkers for CRUD operations
- Calling walkers from frontend with
spawn - Graph traversal (
-->,`?Node) - Creating node connections (
++>) - Reporting data to frontend
- Using
_jac_idfor node references - Data persistence on the server
Common Issues#
Issue: Walker not found#
Check: Is the walker defined OUTSIDE the cl { } block?
# Correct
walker read_todos {
# ...
}
cl {
# frontend code
}
# Wrong
cl {
walker read_todos { # Can't define walkers in frontend!
# ...
}
}
Issue: Empty reports array#
Check: Did you call report in your walker?
# Wrong - no report
can read with `root entry {
visit [-->(`?Todo)];
}
# Correct - report in Todo entry
can report_todos with Todo entry {
report here;
}
Issue: "Cannot read property '_jac_id'"#
Check: Is result.reports empty? Does the todo exist?
# Safe access
if result.reports and result.reports.length > 0 {
let todo = result.reports[0][0];
console.log(todo._jac_id);
}
Issue: Data not persisting#
Check:
- Are you calling the walker? root spawn create_todo(...)
- Is the walker running successfully? Check browser console
- Did you remove the localStorage code? (We don't need it anymore!)
Quick Exercise#
Try adding a walker to clear all completed todos:
walker clear_completed {
can clear with `root entry {
visit [-->(`?Todo)];
}
can delete_if_done with Todo entry {
if here.done {
here.destroy();
}
}
}
# Call from frontend
async def clearCompleted() -> None {
await root spawn clear_completed();
setTodos(todos.filter(lambda todo: any -> bool {
return not todo.done;
}));
}
Next Step#
Excellent! Your app now has a real backend. But there's a problem: everyone can see everyone's todos!
In the next step, we'll add authentication to make your app secure and private!