Quick answer
TL;DR
| Problem | Two independently-built Vue apps, each with its own Pinia stores, risked duplicating API-shaping logic and drifting onto inconsistent store patterns |
| What I did | Migrated all 21 stores (17 admin, 4 frontend) to Composition-API defineStore form, and routed data-shaping through a shared @model/@utils layer |
| Why it mattered | A backend response-shape change now gets fixed once in the model layer, not separately in every store that touched that data |
| Team | Solo, with guidance from Jafran Hasan |
| Part of | A Shared Component Architecture for a WordPress Booking Plugin series → |
Two apps, one state-management problem
- 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
Migrating every store to the Composition API
- 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
Keeping stores thin with a shared model layer
- 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
FAQ
Part of the A Shared Component Architecture for a WordPress Booking Plugin series →