Asia/Dhaka
Case Studies

Centralizing State Management Across a Multi-App Architecture

image
17 admin stores and 4 frontend stores, migrated to Composition-API form and routed through one shared model layer instead of each calling the API directly.
PreBook ships as two separate Vue 3 apps - an admin dashboard and a customer-facing booking flow - each with its own Pinia store tree. I migrated all 21 stores to the Composition-API form, and moved API calls and data-shaping out of the stores into one shared model layer both apps import from. Stores now hold state; the model layer owns the HTTP contract.
21 stores across two apps, migrated to Composition API in December 2023, now routing through a shared BaseModel class instead of per-store API calls.
ProblemTwo independently-built Vue apps, each with its own Pinia stores, risked duplicating API-shaping logic and drifting onto inconsistent store patterns
What I didMigrated all 21 stores (17 admin, 4 frontend) to Composition-API defineStore form, and routed data-shaping through a shared @model/@utils layer
Why it matteredA backend response-shape change now gets fixed once in the model layer, not separately in every store that touched that data
TeamSolo, with guidance from Jafran Hasan
Part ofA Shared Component Architecture for a WordPress Booking Plugin series →
Admin and frontend are separately built, separately deployed Vue apps with no package boundary between them, sharing only a path alias into common source.
  • Admin and frontend are two separate Vue apps, each with its own Vite config and its own bundle
  • No published package between them - they share source through identical path aliases (@components, @model, @utils, ...)
  • That's efficient to ship, but risky for state: two independent store trees mean the same data - an appointment, a staff member - could get fetched and shaped differently in each app
  • A backend change would then need two separate fixes, with no guarantee they stayed consistent
Options-API defineStore with separate state/actions/getters objects, next to the Composition-API setup-function form where refs, computeds, and methods are declared together and returned as one object.
  • The stores started in Pinia's Options form - defineStore('name', { state, actions, getters }) - split into separate objects by category
  • I moved every store in both apps to the Composition form instead: defineStore('name', () => { ... return {...} }), where state, computed values, and methods sit together in one function body
  • Both apps' stores (17 admin, 4 frontend) were converted the same day
  • Why it's better: in the Options form, a fetch method and the ref it populates end up in different parts of the file. The Composition form keeps related state and behavior next to each other - closer to how <script setup> components already read elsewhere in the codebase
A BaseModel class centralizing get, all, update, delete, create, and bulk operations; Appointment and Staff subclasses each set only a route and a title, and every store calls through them instead of calling the API directly.
  • The bigger change wasn't the syntax migration - it was what the stores stopped doing afterward
  • Most stores in both apps now import from @model instead of making API calls themselves: a ref for the list, a computed for reading it, and methods that call into a model class instead of an endpoint directly
  • One BaseModel class defines get, all, create, update, delete, and bulk operations once. Each resource model just sets a route - Appointment and Staff are under 25 lines each, since the CRUD behavior is inherited
  • Why it matters with two apps: without BaseModel, a backend shape change would need fixing in both admin's and frontend's stores separately, with no guarantee they stayed consistent. Routed through one model class, the fix happens once and both apps pick it up on their next build
Four judgment calls: why migrate mid-project instead of leaving working stores alone, why a shared model layer instead of per-store API calls, why a BaseModel class instead of composable fetch functions, and why migrate both apps the same day.
Why migrate to Composition API stores instead of leaving the working Options API stores alone? The Options form was working but getting harder to read as stores grew - a fetch method and the ref it populated lived in different parts of the file. Better to pay that cost once, early, than let it widen. Why route data through a shared model layer instead of letting each store call the API directly? Because there are two apps. Logic written inside one store only protects that store - the other app would need the same fix made separately. Centralizing in BaseModel means one fix, both apps inherit it. Why a BaseModel class instead of shared composable functions? Resources mostly differ by route and label - Appointment and Staff need identical CRUD shapes, just different endpoints. A base class with subclasses setting a route expresses that directly. Why migrate both apps' stores on the same day instead of one at a time? So the two store trees reached the new pattern together, instead of reasoning about two different store shapes while writing features that touched both.
Part of the A Shared Component Architecture for a WordPress Booking Plugin series →

Related case studies

A Shared Component Architecture for a WordPress Booking Plugin - 4 Case Studies

A Shared Component Architecture for a WordPress Booking Plugin - 4 Case Studies

A solo, year-long build of a 44-component Vue library shared between two independently built apps inside a WordPress booking plugin - four linked case studies covering API boundaries, state, resilient input handling, and codebase structure.
Structuring a Large Frontend Codebase for Maintainability at Scale

Structuring a Large Frontend Codebase for Maintainability at Scale

Two independently-built Vue apps and a webpack-built Gutenberg block shared one component library through a consistent alias system and a per-app Tailwind purge - except in the one place plain CSS won.