JS Quickstart
Get a working table view in under five minutes with Express.
Install
pnpm add @retrofit-ui/builder-zod @retrofit-ui/spa-solid-shoelace express
pnpm add -D @types/express tsxbuilder-zod contains the Zod-driven spec builders. spa-solid-shoelace provides the pre-built SPA bundle (exposed as distPath) that your server serves as static files at /. The server's only jobs are to emit spec JSON and serve the bundle — the same model in any language.
Minimum working example
import { TableView } from '@retrofit-ui/builder-zod';
import { distPath } from '@retrofit-ui/spa-solid-shoelace';
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
// Your existing data + schema
const ItemSchema = z.object({
id: z.number(),
name: z.string(),
active: z.boolean(),
});
const items = [
{ id: 1, name: 'First item', active: true },
{ id: 2, name: 'Second item', active: false },
];
// 1. Serve the config endpoint + the SPA bundle as static files
app.get('/retrofit.json', (_req, res) => res.json({ apiBase: '/api/ui' }));
app.use(express.static(distPath));
// 2. Your existing REST endpoint (unchanged)
app.get('/items', (_req, res) => res.json(items));
// 3. The spec endpoint — tells the SPA how to render the table
app.get('/api/ui/items', (_req, res) => {
res.json(
TableView.schema(ItemSchema)
.list({ method: 'GET', url: '/items' })
.build(),
);
});
app.listen(3000, () => console.log('http://localhost:3000'));Run it:
npx tsx server.tsOpen http://localhost:3000 and you'll see a table with Name and Active columns populated from your data. retrofit-ui derived the column types from the Zod schema automatically.
The /api/ui prefix is your choice
apiBase is a prefix retrofit-ui prepends when it fetches specs. /api/ui above is a convention that reads cleanly, not a requirement — use /admin-ui, /ui/v2, or / if that fits your server. Just make sure the apiBase in /retrofit.json matches the routes you register.
Adding create and delete
Wire up more endpoints by adding handlers and endpoint directives to the builder:
// Add REST handlers
app.post('/items', (req, res) => {
const item = { id: items.length + 1, ...req.body };
items.push(item);
res.status(201).json(item);
});
app.delete('/items/:id', (req, res) => {
const idx = items.findIndex((i) => i.id === Number(req.params.id));
if (idx !== -1) items.splice(idx, 1);
res.json({ ok: true });
});
// Update the spec endpoint
app.get('/api/ui/items', (_req, res) => {
res.json(
TableView.schema(ItemSchema)
.list({ method: 'GET', url: '/items' })
.create({ method: 'POST', url: '/items' })
.delete({ method: 'DELETE', url: '/items/{id}' })
.build(),
);
});A "New" button and per-row "Delete" buttons appear automatically.
Next steps
- Inline editing — use
updateSchemato mark which fields are editable directly in the table. See Table View. - Separate form view — use
formSpecfor a dedicated create/edit page. See Form View. - Combined table + form — use
TableFormWorkflowBundlefor the most common pattern. See Workflow Bundle. - Run a full example —
just example js todosstarts the todos example from the repo.