Web LoomWeb.loom
34 packages · 10 published on npm

Business logic that survives
framework changes

MVVM is the architectural discipline. The browser is the platform. Web Loom packages give you clean, typed APIs over mature browser primitives — fetch, storage, routing, events — without heavy abstractions or framework lock-in. Your business logic runs anywhere.

Scaffold a full Web Loom app in one command

`create-web-loom` runs Vite, installs Web Loom packages, and replaces runnable starter files with a polished MVVM starter UI. It supports npm, pnpm, yarn, and bun.

npm create web-loom@latest my-app

Supports Vite starters for React, Vue, Preact, Solid, Svelte, Lit, Vanilla, and Qwik in both TypeScript and JavaScript variants.

See create-web-loom docs

The pattern

One ViewModel.
Every framework.

The ViewModel is plain TypeScript — no framework imports, no heavy runtime. Infrastructure packages sit directly on mature browser APIs: fetch for HTTP, localStorage for persistence, History API for routing. Typed interfaces over what the platform already provides — not replacements for it.

  • Modelowns data, calls browser APIs or your backend
  • ViewModelderives displayable state, exposes Commands
  • Viewsubscribes and renders — the only framework-specific code
Deep dive into core concepts
features/catalog/CatalogModel.tsTS
1export class CatalogModel extends BaseModel<CatalogProductDto[], any> {2  private readonly query = new QueryCore({3    cacheProvider: 'localStorage',4    defaultRefetchAfter: 2 * 60 * 1000,5  });6  private initialized = false;78  private async ensureInitialized(): Promise<void> {9    if (this.initialized) return;10    await this.query.defineEndpoint('catalog:products:v3', () => this.api.listProducts());11    this.query.subscribe('catalog:products:v3', (state) => this.syncFromQueryState(state));12    this.initialized = true;13  }1415  async fetchAll(forceRefetch = false): Promise<void> {16    await this.ensureInitialized();17    await this.query.refetch('catalog:products:v3', forceRefetch);18  }1920  private syncFromQueryState(state: EndpointState<CatalogProductDto[]>): void {21    this.setLoading(state.isLoading);22    if (state.data) this.setData(state.data);23    if (state.error) this.setError(state.error);24  }25}
1export class CatalogViewModel extends ActiveAwareViewModel<CatalogModel> {2  private readonly productsState = signal<CatalogProductDto[]>([]);3  private readonly searchState = signal('');45  readonly filteredProducts = computed(() => {6    const q = this.searchState.get().trim().toLowerCase();7    const items = this.productsState.get();8    return q ? items.filter((p) => p.name.toLowerCase().includes(q)) : items;9  });1011  constructor(model: CatalogModel) {12    super(model);13    this.addSubscription(this.model.data$.subscribe((items) => this.productsState.set(items ?? [])));14  }1516  readonly refreshCatalogCommand = this.registerCommand(17    new Command(async () => this.model.refresh()),18  );1920  setSearchQuery(value: string): void {21    this.searchState.set(value);22  }23}
1function CatalogRoute() {2  const products = useSignalValue(catalogViewModel.filteredProducts);3  const totalProducts = useSignalValue(catalogViewModel.totalProducts);4  const selectedProduct = useSignalValue(catalogViewModel.selectedProduct);5  const searchQuery = useSignalValue(catalogViewModel.searchQuery);6  const isLoading = useObservable(catalogViewModel.isLoading$, false);78  return (9    <ProductBrowser10      products={products}11      totalProducts={totalProducts}12      selectedProduct={selectedProduct}13      isLoading={isLoading}14      searchQuery={searchQuery}15      onSearchChange={(value) => catalogViewModel.setSearchQuery(value)}16      onRefresh={() => void catalogViewModel.refreshCatalogCommand.execute()}17      onAddToCart={(productId) => void cartViewModel.addToCartCommand.execute({ productId, quantity: 1 })}18    />19  );20}
Model - Owns API/cache lifecycle and pushes canonical data state.Real excerpts from apps/ecommerce-mvvm/src

Platform-first

The browser has matured.
Web Loom works with it, not around it.

Every infrastructure package is a thin, typed wrapper over a stable browser primitive. No proprietary runtimes. No invented protocols. Full tree-shaking.

fetchhttp-core

Typed requests with interceptors and response mapping

localStoragestorage-core

Versioned, typed persistence with session and memory adapters

History APIrouter-core

Reactive navigation state over pushState and popstate

EventTargetevent-emitter-core

Strongly typed event emitter with automatic disposal

M
Technical Book
23 Chapters

MVVM in Practice

A practical, code-first guide to Model-View-ViewModel architecture for modern frontend development.

FoundationsCore PatternsFramework ImplementationsAdvanced Topics

Learn MVVM

Master framework-agnostic architecture

Learn MVVM patterns that work across React, Vue, Angular, Lit, and vanilla JavaScript. Every example is extracted from real, production-ready code in the Web Loom monorepo.

  • Build Models that encapsulate business logic
  • Create ViewModels that manage presentation state
  • Implement Views that remain purely presentational
  • Apply patterns like reactive state, event-driven communication, and design systems
Start reading

Compatibility

Works with every major framework

The ViewModel has no framework imports. Connecting it to a new framework means writing one thin subscription bridge — typically under 20 lines.

The author

Festus Yeboah

Framework Architect

Festus is a frontend architect who has spent years watching teams rewrite the same business logic every time the framework pendulum swings. Web Loom is his attempt to give the web the same architectural continuity that Android, iOS, and .NET have enjoyed for twenty years.

The project is open-source and driven by the conviction that the 80% of an application that is not rendering code should be portable, testable, and immune to framework churn.

Follow on GitHub

Ready to scaffold and ship faster?

Start with the CLI starter, then evolve your ViewModels without framework lock-in.

npm create web-loom@latest my-app