Skip to content

Large Apps

Building something big on Manifest — a product with many screens, sign-in, lots of data? Here's how to set it up so it stays fast and easy to change.


Organize Your Code

Once your scripts grow past a few hundred lines, split them into small files that import each other. Browsers do this natively with JavaScript modules, so there's still no build step. An import map gives your folder a short name.

Before: one script holds the cart state, list behaviour, summary behaviour and formatPrice. Before script.js cart state list behaviour summary behaviour formatPrice() After: main.js imports a cart store and two components, and both components import a money helper. After main.js cart.js item-list.js cart-summary.js money.js
Arrows point from a file to what it imports. Each file lists what it needs at the top.

This demo is built exactly that way. Every name here is a stand-in: call the folder, the files and the prefix whatever you like.

<script type="importmap">
  { "imports": { "@js/": "/js/" } }
</script>
<script src="https://cdn.jsdelivr.net/npm/mnfst@latest/lib/manifest.min.js"></script>
<script type="module" src="/js/main.js"></script>

Register components and stores in an alpine:init listener. Import each file by a single URL: the same file at a different URL, such as with ?v=2, loads a second copy with its own state.


Share Code with Your Server

If a server, such as a Cloudflare Worker, checks the same things as the page, put those rules in one file that both import. The page and the server then can't disagree, and a change is one edit.

export const maxTitle = 80

export function checkTitle(title) {
  if (!title.trim()) return 'required'
  if (title.length > maxTitle) return 'too-long'
}

Everything the page loads is public, so keep secrets in server code only.


Load Large Tables

An Appwrite table shows its first 25 rows unless you ask for more. Set paging on the data source and Manifest loads pages as people scroll, keeping each one live. Filter on the server with $query, and save with $update: the change shows at once and rolls back if it fails.

<div x-virtual class="h-[480px] overflow-auto" @virtual-end="$x.tasks.$more()">
  <template x-for="task in $x.tasks" :key="task.$id">
    <label>
      <input type="checkbox" :checked="task.done" @change="$x.tasks.$update(task.$id, { done: $el.checked })">
      <span x-text="task.title"></span>
    </label>
  </template>
</div>
manifest.json
{ "data": { "tasks": { "appwriteTableId": "tasks", "paging": { "mode": "window", "size": 100 } } } }

See large tables for every option.


Catch Mistakes Early

A misspelt name often fails only when someone clicks. mnfst-types reads your scripts, without changing them, and points at the mistake first.

npx mnfst-types
npx mnfst-types --tsc

The first command writes manifest.d.ts, the types for Manifest itself; run it again when your data sources change. The second checks:

js/cart-summary.js(1,10): error TS2724: '"@js/money.js"' has no exported member named 'formatPrise'. Did you mean 'formatPrice'?

It checks the scripts your page loads and everything they import. See type checks.


Test Like a User

List the few things people must always be able to do, like signing in or adding an item, and let mnfst-test do them in a real browser (it uses puppeteer). It saves a screenshot wherever you call snap and fails when one changes. Install it with npm install -D mnfst-test puppeteer.

journeys.mjs
export default { root: '.', journeys: [{ name: 'add an item', async steps(j) { await j.page.click('button') await j.snap('added') } }] }
npx mnfst-test journeys journeys.mjs

See golden journeys.