2. Store factory + endpoints#

What this step does. Configures the single source of truth for network calls — every grid, form, and lookup on the frontend goes through this factory. Pages never import axios or fetch.

One factory, one error handler, one central endpoint config. Every page fetches through this.

Store factory#

// src/wire/StoreFactory.ts
import { PalmyraStoreFactory } from '@palmyralabs/palmyra-wire';
import { toast } from 'react-toastify';

const errorHandler = () => (error: any) => {
  const status = error?.response?.status;
  if (status === 401) { window.location.assign('/login'); return true; }
  if (status === 403) { toast.error("You don't have permission for this action."); return true; }
  if (status >= 500)  { toast.error('Server error. Please retry.'); return true; }
  return false;
};

const AppStoreFactory = new PalmyraStoreFactory({
  baseUrl: '/api',
  errorHandlerFactory: errorHandler,
});

export default AppStoreFactory;

baseUrl: '/api' matches the SpringBoot context path set in the backend setup.

Endpoint config#

Keep every resource path in one file — if the backend renames a @CrudMapping, only this module changes.

// src/config/ServiceEndpoints.ts
export const ServiceEndpoint = {
  department: {
    restApi: '/department',
    byId:    '/department/{id}',
  },
  employee: {
    restApi: '/employee',
    byId:    '/employee/{id}',
  },
};

// Lookup endpoints (for MantineServerLookup / ServerLookup fields)
export const LookupEndPoint = {
  department: '/department',
};

Shared field-error messages#

The clinic sample centralises the invalidMessage strings so every form reads from one source:

// src/config/ErrorMsgConfig.ts
export const fieldMsg = {
  mandatory: 'This field is required',
  email:     'Please enter a valid email address',
};

No hand-rolled grid helper needed#

Because SummaryGrid (from @palmyralabs/template-tribble) asks the store factory for its grid store on its own, pages don’t need a custom useGrid / useGridstore wrapper. The factory is resolved through StoreFactoryContext — set up in the bootstrap step — and SummaryGrid takes the endpoint path plus a gridRef and handles query, paging, sort, filter, and refresh internally.

Using a store directly#

Most of the time SummaryGrid / DialogNewForm / MantineServerLookup do the fetching for you. When you need something bespoke — a chart, a batch action, a dashboard tile — reach for the factory directly. Three common shapes:

import AppStoreFactory from './StoreFactory';

// Read a single record
async function getDepartment(id: string | number) {
  const store = AppStoreFactory.getFormStore({}, '/department/{id}', 'id');
  return store.fetchData({ id });
}

// Save a record (insert or update — depends on whether the body has an id)
async function saveDepartment(data: any) {
  const store = AppStoreFactory.getFormStore({}, '/department/{id}', 'id');
  return store.saveData(data);
}

// Run a custom query from a dashboard widget
async function countActiveEmployeesByDepartment(code: string) {
  const store = AppStoreFactory.getGridStore({}, '/employee');
  const { total } = await store.queryData({
    status:            'ACTIVE',
    'department.code': code,
    _limit:            0,         // rows not needed
    _total:            true,      // count is what we want
  });
  return total;
}

Each call comes back as a Promise. Errors flow through the factory’s errorHandler, so 401 / 403 / 5xx are already surfaced as toasts or redirects — your component code only handles the happy path.