Backend Loop
Build a small API with a server and no pages (the api starter): a table made by a migration, routes that read and write it, a job the routes queue, and a job on a schedule.
1. Scaffold the app
webstir init api my-backend
cd my-backend
bun installThe app has src/backend/index.ts (the server), .env.example (its settings, all optional in development) and a .gitignore that keeps data/ and .webstir/ out of git.
2. Run it
webstir watch --workspace "$PWD"The server restarts whenever a file under src/backend/ changes.
3. Make a table
webstir add-migration create-notes --workspace "$PWD"Write the table in src/backend/migrations/0001-create-notes.sql:
CREATE TABLE notes (
id TEXT PRIMARY KEY,
body TEXT NOT NULL,
created_at TEXT NOT NULL
);When the server restarts, it applies the migration: SQLite at data/app.sqlite is created, and the table in it.
4. Read and write it
Add src/backend/module.ts:
const listNotes = {
definition: { name: 'listNotes', method: 'GET', path: '/api/notes' },
handler: async (ctx) => ({
body: { notes: await ctx.db.query('SELECT id, body FROM notes ORDER BY created_at DESC') },
}),
};
const addNote = {
definition: { name: 'addNote', method: 'POST', path: '/api/notes' },
handler: async (ctx) => {
const body = String((ctx.body as { body?: unknown })?.body ?? '');
const id = crypto.randomUUID();
await ctx.db.execute('INSERT INTO notes (id, body, created_at) VALUES (?, ?, ?)', [id, body, new Date()]);
await ctx.jobs.enqueue('index-note', { id });
return { status: 201, body: { id } };
},
};
const routes = [listNotes, addNote];
export const module = {
manifest: {
contractVersion: '1.0.0',
name: 'my-backend',
version: '0.1.0',
kind: 'backend',
routes: routes.map((route) => route.definition),
},
routes,
};curl -X POST localhost:4321/api/notes -H 'content-type: application/json' -d '{"body":"hello"}'
curl localhost:4321/api/notes5. Add the jobs
The route queues index-note; make it, and a nightly clean-up:
webstir add-job index-note --workspace "$PWD"
webstir add-job prune --workspace "$PWD" --schedule "0 3 * * *"// src/backend/jobs/index-note/index.ts
import { db } from '@webstir-io/webstir-backend/db';
export async function run(payload: { id: string }) {
const note = await db.get('SELECT body FROM notes WHERE id = ?', [payload.id]);
console.info('[index-note]', note);
}Both run inside the server: index-note from the queue, retried if it throws, and prune at 3 a.m. See them, or run one now:
webstir jobs --workspace "$PWD"
webstir jobs run prune --workspace "$PWD"6. Inspect and publish
webstir backend-inspect --workspace "$PWD"
webstir publish --workspace "$PWD"backend-inspect prints the routes, jobs and migrations. In production, set SESSION_SECRET and keep data/ on a disk that persists (see Environment).