Skip to content

Frontend Architecture (FSD)

MedRegia follows Feature-Sliced Design — a methodology for structuring large frontend applications around features and domains rather than technical concerns.

Layer Responsibilities

app/ — App Shell (wiring only)

  • Router configuration (createRouter)
  • Global plugin registration (Pinia, Vue Router)
  • Global CSS imports
  • No business logic, no API calls

pages/ — Route Shells (~50 lines max)

  • One file per route
  • Composes widgets and features
  • Passes route params down; never contains logic itself
vue
<!-- pages/vendors/VendorsPage.vue — example -->
<script setup lang="ts">
import { VendorListFeature } from '@/features/vendors'
</script>

<template>
  <AppLayout>
    <VendorListFeature />
  </AppLayout>
</template>

widgets/ — Layout Blocks

Composite components used across multiple pages:

WidgetDescription
AppLayout.vueMain layout wrapper — sidebar + header + content slot
AppSidebar.vueNavigation sidebar with route links and permission-gated items
AppHeader.vueTop bar — current page title, user menu, quick actions
QuickActions.vueFloating shortcut menu for common actions

features/ — Domain Slices

Each feature is a self-contained slice for one bounded context.
Every feature exposes only through its index.ts barrel.

FeatureExports
auth/LoginForm, useAuth, auth store
vendors/VendorList, VendorForm, VendorDetail, useVendors
invoices/InvoiceList, InvoiceForm, InvoiceDetail, useInvoices
transactions/TransactionList, TransactionForm, useTransactions
dashboard/DashboardSummary, useDashboard
settings/SettingsList, useSettings
reports/ReportDownloadButtons
profile/ProfileForm, ChangePasswordForm
onboarding/OnboardingWizard

shared/ — Cross-Cutting Primitives

The shared/ layer is used by every other layer. It must remain dependency-free (no imports from features or pages).

shared/api/

typescript
// HTTP client factory — never import axios directly outside this folder
export const client: IHttpClient = createHttpClient(...)

// API factories — one per backend module
export const vendorApi = createVendorApi(client)
export const invoiceApi = createInvoiceApi(client)

shared/composables/

ComposablePurpose
useAuthLogin, logout, token storage, current user
usePermissioncan(collection, action) — reads JWT perm claims
useApiGeneric async state wrapper (loading, error, data)
useToastShow success/error toast notifications
usePaginationPage number, pageSize, total state
useFiltersFilter state + debounced URL sync
useCurrencyFormat amounts using the Currency setting
useConfirmDialogReusable confirm-before-delete dialog
useInvoiceNavNavigate to invoice detail/edit
useVendorNavNavigate to vendor detail/edit

API Client Pattern

All API calls use the createXxxApi(client) factory pattern:

typescript
// shared/api/vendors.ts
export const createVendorApi = (client: IHttpClient) => ({
  getVendors: (params: VendorListParams) =>
    client.get<PagedResult<VendorDto>>('/vendors', { params }),

  getVendorById: (id: number) =>
    client.get<VendorDto>(`/vendors/${id}`),

  createVendor: (data: CreateVendorDto) =>
    client.post<VendorDto>('/vendors', data),

  updateVendor: (id: number, data: UpdateVendorDto) =>
    client.put<VendorDto>(`/vendors/${id}`, data),

  deleteVendor: (id: number, reason?: string) =>
    client.delete(`/vendors/${id}`, { data: { reason } })
})

Error Handling

All errors from the API are normalised through normalizeError(err) in shared/api/errors.ts:

typescript
// In any composable
try {
  await vendorApi.createVendor(data)
} catch (err) {
  const apiError = normalizeError(err)
  toast.error(apiError.title ?? 'Something went wrong')
}

Raw unknown errors must never propagate past the composable/store layer.

MedRegia — Pharmaceutical Vendor Management Platform