Client work · 2025 — Present
Parfetts
Cash & Carry digital platform for independent retailers
- Next.js 16
- React 19
- TypeScript
- Chakra UI 3
- Zod
- Zustand
- Algolia
- PostHog
- Pusher
- Bun
The problem
Parfetts is an employee-owned Cash & Carry wholesaler whose customers are independent retailers — corner shops and forecourts running on thin margins and thinner time. That means two very different jobs on the web: a marketing site that explains the business to prospective retailers and members, and an ordering platform those retailers use to actually place trade orders. Treated as separate projects they drift apart; treated as one project the ordering flows get buried under brochure pages.
The approach
Two Next.js App Router applications sharing one design system. Chakra UI 3 semantic tokens mean a colour or spacing decision is made once and inherited by both, so the two sites stay recognisably Parfetts without being forced into the same layouts. Everything crossing the network boundary is typed: a REST fetch client with Zod schemas validating responses, and the same schemas backing react-hook-form on the way in.
The outcome
Both properties are live and maintained in parallel — the marketing site at parfetts.co.uk and the ordering platform at online.parfetts.co.uk — on a shared component and token layer, with lint, formatting, unit tests and component review enforced on every commit rather than left to good intentions.
Architecture moves
- Next.js App Router across both properties, so routing, layouts and data loading follow one set of conventions.
- REST consumed through a single typed fetch client (src/utils/api) — deliberately not GraphQL, because the API surface did not justify the extra layer.
- Chakra UI 3 with semantic tokens in src/theme as the shared design system between the marketing site and the ordering platform.
- Zod DTOs validate API responses at the boundary, and the same schemas drive react-hook-form validation so client and server agree on shape.
- Zustand for the client state that genuinely needs to be shared across routes, rather than lifting everything into a global store.
- Algolia powers catalogue search, wired in per route so pages that do not search do not pay for it.
- PostHog for product analytics and Pusher for real-time updates.
Performance levers
- Turbopack for local development and builds, which keeps the feedback loop fast on a codebase this size.
- Client components kept as islands around the interactive parts, so static and server-rendered content is not dragged into the browser bundle unnecessarily.
- Motion restricted to transform and opacity, which the compositor can handle without triggering layout.
- Search indexing scoped to the routes that use it rather than loaded globally.
Engineering impact
- Marketing and ordering flows are reliable across two public properties maintained side by side.
- One shared token and component layer means brand changes land in both sites without a manual sweep.
- Typed boundaries mean malformed API responses fail loudly at the edge instead of quietly rendering empty UI.
- Biome, Vitest, Storybook and Lefthook make the quality bar automatic rather than a review-time argument.
Challenges conquered
- Balancing two different UX jobs — persuasion on the marketing site, speed and accuracy on the ordering platform — without letting either dictate the other's layout.
- Keeping design system consistency across two codebases, which is where semantic tokens earn their keep over hard-coded values.
- Deciding where search belongs, since making an entire catalogue searchable everywhere costs more than it returns.