Tutorial: your first web app
You'll build a shared notes page: type a note, press Enter, and everyone sees it. It has a database, server code and browser code, and no routes, fetch calls or API layer.
1. Make a codebase with the server library
With tcm installed:
mkdir notes && cd notes
tcm init
tcm lib add tcm server --at notes.lib.server
The last line mounts the server library tcm ships (tables, sessions, jobs), read-only, at notes.lib.server. (tcm init app makes a small working app
instead.)
2. Write the app
Put this in scratch.ts:
// tcm: namespace notes
import { table } from "#codebase/notes/lib/server";
export type Note = { text: string; at: Date };
/** Every note anyone has written. */
export const notes = table<Note>("notes");
export async function addNote(text: string): Promise<void> {
await notes.insert({ text, at: new Date() });
}
export async function listNotes(): Promise<string[]> {
return (await notes.find({}, { orderBy: "at", desc: true })).map((r) => r.value.text);
}
export async function main() {
const input = document.createElement("input");
const list = document.createElement("ul");
const show = async () => {
list.replaceChildren(...(await listNotes()).map((t) => Object.assign(document.createElement("li"), { textContent: t })));
};
input.onkeydown = async (e) => {
if (e.key !== "Enter") return;
await addNote(input.value);
input.value = "";
await show();
};
document.body.append(input, list);
await show();
}
table<Note>("notes")is a typed table: rows are stored on the server and checked againstNotewhen written. Dates come back as dates.addNoteandlistNotesuse the table;mainusesdocument. Nothing says which runs where.maincallslistNotes()andaddNote()like local functions.
3. Add it
$ tcm add + new notes.Note type + new notes.notes : Table<Note> + new notes.addNote : (text: string) => Promise<void> + new notes.listNotes : () => Promise<string[]> + new notes.main : () => Promise<void>
4. See where things run
$ tcm view notes.addNote // notes.addNote #2w6iz4qg : (text: string) => Promise<void> // runs on the server: it uses notes.notes, which uses notes.lib.server.table, which uses notes.lib.server.Table, which uses #tcm/host (database); browser code calls it by hash $ tcm view notes.main // notes.main #o2wxk6e6 : () => Promise<void> // runs in the browser: it uses document
tcm infers placement from what code uses:
- the database, Node or the server library: the server;
- the DOM: the browser;
- anything else: wherever it's called from.
A browser call to a server function becomes a request to that exact function, by hash. Server functions must be
async, since a network call can't return at once; tcm tells you if one isn't.
5. Run it
$ tcm dev notes.main tcm dev notes.main #o2wxk6e6 url http://127.0.0.1:5173/ browser 1 definition(s) server 2 function(s) the browser calls by hash: notes.addNote #2w6iz4qg, notes.listNotes #o3tcbfex
Open the URL, type a note and press Enter. A second tab shows the same notes: they're in the table, on the server.
The server line lists the app's only endpoints. Nothing else on the server can be called from outside, and each
endpoint checks its arguments against its parameter types before it runs.
In VS Code, the Apps view's ▶ button next to notes.main does the same and opens the page beside your code.
6. Change it
Leave tcm dev running. Write a new listNotes in scratch.ts that shows the newest note in capitals:
// tcm: namespace notes
import { notes } from "#codebase/notes";
export async function listNotes(): Promise<string[]> {
return (await notes.find({}, { orderBy: "at", desc: true })).map((r, i) => (i === 0 ? r.value.text.toUpperCase() : r.value.text));
}
$ tcm update ~ update notes.listNotes : () => Promise<string[]> (was #o3tcbfex) ~ updated notes.main (dependent)
main calls listNotes, so it was updated too. The page reloads with the change: the dev server follows the
codebase, with nothing to restart or rebuild.
7. Build it
$ tcm build notes.main built notes.main #o2wxk6e6 into .tcm/apps/o2wxk6e6ghnnvykwiutwzxn6fz33byqvl2jj672b6z5o4wwa57lq browser 1 definition(s) server 2 function(s) the browser calls by hash: notes.addNote #2w6iz4qg, notes.listNotes #o3tcbfex
A production build is immutable and named by the app's hash.
8. Serve it
tcm start notes.main --port 8080
tcm start serves whatever notes.main names and keeps watching the name. After tcm update it builds the new
version and switches to it; after tcm reset it switches back. Deploying and rolling back are both moving a name.
Open pages keep using the version they loaded until they reload, so an update never breaks someone mid-session.
Next
- The polls example adds sessions, a background job, a recurring job, a live stream of results, tests and documented code.
- Guides: server functions (sessions, errors, streaming), data (queries, changing a table's shape), background jobs, deploying.