[← Back to README](./README.md)
# Guide
This guide builds a todo app one concept at a time. See the [Reference](./REFERENCE.md) for full options on each step, helper, and field. Nerack discovers assets by file location and seeds each one into the context of the module that owns it (see [Assets](./REFERENCE.md#assets)).
* [1. Pages and Templates](#1-pages-and-templates)
* [2. Show Data](#2-show-data)
* [3. Accept Input](#3-accept-input)
* [4. Nested Data](#4-nested-data)
* [5. Calling APIs](#5-calling-apis)
* [6. Tasks](#6-tasks)
* [7. Modules and Events](#7-modules-and-events)
### 1. Pages and Templates
Each `http(...)` declares a named resource and URL, and each verb pipeline is a list of steps. `html("home", "home_s")` renders the template asset `home` into context key `home_s`, and `http_response("home_s")` sends it. `{{url:verb:name}}` builds a link to a resource: the verb first, then the resource name. Any `:params` in the pattern are filled from the current scope by matching key names.
Both pages share a layout, so `home` doubles as the layout: it declares the nav and a `{{$body}}` block whose default is the welcome page. The `todos` page extends it with `{{< home}}...{{/home}}`, overriding that block. Any template that declares a `{{$block}}` can be a parent; there is no special layout type.
**`home.mustache.html`**
```html
{{/body}}
{{/home}}
```
**`todo.c`**
```c
#include
#include
module(todo){
http("home", "/",
.get = {
html("home", "home_s"),
http_response("home_s")
}
);
http("todos", "/todos",
.get = {
html("todos", "todos_s"),
http_response("todos_s")
}
);
}
```
See [HTTP Pipelines](./REFERENCE.md#http-pipelines) and [Templates](./REFERENCE.md#templates).
### 2. Show Data
Bring in SQLite with `#include `, declare a database with `sqlite(...)`, and read with `sqlite_query()`. SQL files are assets like templates: `get_todos.sql` becomes the asset `get_todos`.
Three new SQL files:
**`create_todos_table.sql`**
```sql
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL
);
```
**`seed_todos.sql`**
```sql
INSERT INTO todos(title) VALUES('Learn Nerack');
```
**`get_todos.sql`**
```sql
select id, title from todos;
```
Render the rows Nerack stores under `todos_data`:
**`todos.mustache.html`**
```diff
{{< home}}
{{$body}}
My Todos
-
Nothing yet.
+
+ {{#todos_data}}
+
{{title}}
+ {{/todos_data}}
+
{{/body}}
{{/home}}
```
Wire up the module, database, and query:
**`todo.c`**
```diff
#include
#include
+#include
module(todo){
+ sqlite(
+ "todos_db",
+ "file:todos.db?mode=rwc",
+ {"create_todos_table"},
+ {"seed_todos"}
+ );
+
http("home", "/",
.get = {
html("home", "home_s"),
http_response("home_s")
}
);
http("todos", "/todos",
.get = {
+ sqlite_query({"todos_db", "get_todos", "todos_data"}),
html("todos", "todos_s"),
http_response("todos_s")
}
);
}
```
The three positional arguments are the database name, the SQL asset, and the context key for the result table (`todos_data`). The template walks the result with `{{#todos_data}}...{{/todos_data}}`. Migrations and seeds run on first connection. See [Databases](./REFERENCE.md#databases) and [query](./REFERENCE.md#query).
### 3. Accept Input
Add a `.post` verb that validates, inserts, and redirects (POST-redirect-GET). A resource-scoped `.errors` handler re-renders the form on validation failure.
**`create_todo.sql`**
```sql
insert into todos(title) values({{title}});
```
Add the form, repopulating the field and showing the error after a failed submit:
**`todos.mustache.html`**
```diff
{{< home}}
{{$body}}
My Todos
+
{{#todos_data}}
{{title}}
{{/todos_data}}
{{/body}}
{{/home}}
```
Add a `.post` verb and an `.errors` handler:
**`todo.c`**
```diff
#include
#include
#include
module(todo){
sqlite(
"todos_db",
"file:todos.db?mode=rwc",
{"create_todos_table"},
{"seed_todos"}
);
http("home", "/",
.get = {
html("home", "home_s"),
http_response("home_s")
}
);
http("todos", "/todos",
.get = {
sqlite_query({"todos_db", "get_todos", "todos_data"}),
html("todos", "todos_s"),
http_response("todos_s")
- }
+ },
+ .post = {
+ input({"title", not_empty_input}),
+ sqlite_query({"todos_db", "create_todo"}),
+ http_redirect("todos")
+ },
+ .errors = {
+ {http_bad_request, {http_reroute("todos")}}
+ }
);
}
```
The form posts to `{{url:post:todos}}`. Because `post` is a state-changing verb, the helper appends both `http_method=post` and a fresh CSRF token to the action URL, so the form needs no hidden fields of its own.
`input()` validates and promotes `title` to app scope; the `{{title}}` in `create_todo.sql` binds as a prepared-statement parameter. On failure, `http_bad_request` triggers the handler, which `reroute`s back into the GET pipeline in-process. The `input:` and `error:` scopes survive the reroute, so the form repopulates with `{{input:title}}` and shows `{{error_message:title}}`. See [input](./REFERENCE.md#input), [Error and Repair Pipelines](./REFERENCE.md#error-and-repair-pipelines), and [redirect and reroute](./REFERENCE.md#redirect-and-reroute).
### 4. Nested Data
A `/todos/:id` page fetches a todo and its comments concurrently, then nests the comments inside the todo with `join()`.
Three new SQL files and one new template:
**`create_comments_table.sql`**
```sql
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
todo_id INTEGER NOT NULL REFERENCES todos(id),
body TEXT NOT NULL
);
```
**`get_todo.sql`**
```sql
select id, title from todos where id = {{id}};
```
**`get_comments.sql`**
```sql
select id, todo_id, body from comments where todo_id = {{id}};
```
Enter `{{#todo_data}}` first; after the join, `comments` lives inside each todo record:
**`todo.mustache.html`**
```html
{{< home}}
{{$body}}
{{#todo_data}}
{{title}}
Comments
{{#comments}}
{{body}}
{{/comments}}
{{/todo_data}}
{{/body}}
{{/home}}
```
Link each list item to its detail page. `{{url:get:todo}}` resolves the `todo` resource's pattern (`/todos/:id`) and fills `:id` from the current scope, which inside `{{#todos_data}}` is the current row:
**`todos.mustache.html`**
```diff
{{#todos_data}}
-
{{/quote}}
{{/body}}
```
The todos logic moves into the module unchanged, with a `publish()` declaration and an `emit()` step added. Both todo resources move with it:
**`todos/todos.c`**
```c
#include
#include
#include
#include
#include
module(todos){
sqlite(
"todos_db",
"file:todos.db?mode=rwc",
{"create_todos_table", "create_comments_table", "create_daily_stats_table"},
{"seed_todos"}
);
publish("todo_created",
.with = {"title"}
);
task("record_daily_stats", {
sqlite_query({"todos_db", "record_daily_stats"})
}, .cron = "0 0 * * *");
task("notify_new_todo", {
http_fetch({
http_post,
"https://api.push.dev/notify",
.json = "{\"text\":\"New todo: {{title}}\"}"
})
}, .accepts = {"title"});
http("todos", "/todos",
.get = {
sqlite_query({"todos_db", "get_todos", "todos_data"}),
html("todos", "todos_s"),
http_response("todos_s")
},
.post = {
input({"title", not_empty_input}),
sqlite_query({"todos_db", "create_todo"}),
dispatch("notify_new_todo"),
emit("todo_created"),
http_redirect("todos")
},
.errors = {
{http_bad_request, {http_reroute("todos")}}
}
);
http("todo", "/todos/:id",
.get = {
input({"id", positive_integer_input}),
sqlite_query(
{"todos_db", "get_todo", "todo_data", .error_on_empty = true},
{"todos_db", "get_comments", "comments"}
),
join("todo_data", "id", "comments", "todo_id"),
html("todo", "todo_s"),
http_response("todo_s")
}
);
}
```
The `activity` module owns its own table, query, template, and subscriber. Nothing in it references the todos module:
**`activity/create_activity_table.sql`**
```sql
CREATE TABLE IF NOT EXISTS activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
ref TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
**`activity/insert_activity.sql`**
```sql
insert into activities(kind, ref) values('created', {{title}});
```
**`activity/get_activities.sql`**
```sql
select kind, ref, created_at from activities order by created_at desc;
```
**`activity/activity.mustache.html`**
```html
{{< home}}
{{$body}}
Activity
{{#activities}}
{{kind}}: {{ref}} ({{created_at}})
{{/activities}}
{{/body}}
{{/home}}
```
**`activity/activity.c`**
```c
#include
#include
#include
#include
module(activity){
sqlite(
"activity_db",
"file:activity.db?mode=rwc",
{"create_activity_table"}
);
subscribe("todo_created", {
sqlite_query({"activity_db", "insert_activity"})
});
http("activity", "/activity",
.get = {
sqlite_query({"activity_db", "get_activities", "activities"}),
html("activity", "activity_s"),
http_response("activity_s")
}
);
}
```
When the POST calls `emit("todo_created")`, Nerack propagates the keys named in `publish(...).with` (`title`) to every subscriber. The `activity` module writes its row with no direct link to the publisher. Events are durable: undelivered ones replay after a crash. A third subscriber is another module with its own `subscribe(...)`, and the publisher still does not change. See [Modules and Composition](./REFERENCE.md#modules-and-composition) and [Event Pipelines](./REFERENCE.md#event-pipelines).