Skip to main content

Copying features into your project

Foundation is organised so that features are self-contained and easy to move. This "lift-and-shift" structure makes it straightforward to copy a feature into a new app with minimal changes.

Why we use this structure

  • Implement one feature with minimal effort – Copy the feature folder, fix imports, and integrate. No need to pull in the whole app.
  • Clear boundaries – Each feature owns its GraphQL, hooks, UI, and mocks in one place.
  • Reusability – Copy or extract a feature without hunting through a flat hooks/, graphql/, or components/ tree.
  • Consistent layout – The same folders (graphql/queries, graphql/mutations, client/hooks, etc.) in every feature so you know where to add or find code.
  • Simpler lift-and-shift – Move a feature by copying the folder and fixing imports, not by reassembling scattered files.

Standard feature layout

Feature-specific code lives under apps/foundation/src/features/. Every feature there follows this pattern:

features/
<feature-name>/
graphql/
queries/ # GraphQL query documents (one file per query)
mutations/ # GraphQL mutation documents (one file per mutation)
resolvers/ # Optional: mock GraphQL resolvers
fragments/ # Optional: shared fragments
client/
hooks/ # React hooks (data fetching, handlers)
hooks/mocks/ # MSW handlers for tests/Storybook
components/ # Feature-specific UI components
pages/ # Optional: feature-specific page components
utils/ # Optional: feature-only utilities
  • GraphQL lives at the feature rootgraphql/queries and graphql/mutations are the single place for that feature's operations.
  • Client code is under client/ – Hooks, components, pages, and mocks are grouped under client/ so "everything that runs in the browser" is in one subtree.

Shared code

Code used by multiple features (e.g. viewer query, account-ledger hooks) lives under apps/foundation/src/shared and uses the same folder conventions:

shared/
graphql/
queries/ # e.g. viewer, pendingPayments, accountLedgers
resolvers/ # Optional
mocks/ # Optional
client/
hooks/ # e.g. useViewer, useAccountLedgers
components/ # Optional
utils/ # Optional
tip

When lifting a feature that imports from @foundation/shared/..., copy the shared modules it needs (or replace them with your app's equivalents).

Example: Lift-and-shift layout in practice (comms preferences)

This section walks through the comms preferences feature to show how the standard layout looks in a real feature—one query, one mutation, hooks, and UI in one folder. It has no external dependencies like Stripe, only Kraken API and React.

Location: apps/foundation/src/features/dashboard/comms-preferences/

1. GraphQL at the feature root

All Kraken operations for this feature are in graphql/queries and graphql/mutations:

Query – get preferences
graphql/queries/commsPreferences.ts:

import { graphql } from "@foundation/gql-tada";

export const commsPreferences = graphql(`
query commsPreferences {
viewer {
preferences {
isOptedInMeterReadingConfirmations
isOptedInToSmsMessages
isUsingInvertedEmailColours
}
}
}
`);

Mutation – update preferences
graphql/mutations/updateCommsPreferences.ts:

import { graphql } from "@foundation/gql-tada";

export const updateCommsPreferences = graphql(`
mutation updateCommsPreferences(
$input: UpdateAccountUserCommsPreferencesMutationInput!
) {
updateCommsPreferences(input: $input) {
commsPreferences {
isOptedInMeterReadingConfirmations
isOptedInToSmsMessages
isUsingInvertedEmailColours
}
}
}
`);

Hooks and components import these documents; they do not define GraphQL inline. That keeps the feature easy to move: you take the graphql/ folder and the same operations are available in the new app.

2. Hooks under client/hooks

Hooks only orchestrate data and call the API; they import the documents from the feature's graphql/ folder:

client/hooks/useCommsPreferences.ts – uses the query:

import { commsPreferences } from "@foundation/features/dashboard/comms-preferences/graphql/queries/commsPreferences";
import { useKrakenQuery } from "@krakentech/blueprint-api/client";

export const useCommsPreferences = () => {
return useKrakenQuery({
document: commsPreferences,
queryKey: ["comms-preferences"],
select: (data) => data?.viewer?.preferences,
});
};

client/hooks/useUpdateCommsPreferences.ts – uses the mutation:

import { updateCommsPreferences } from "@foundation/features/dashboard/comms-preferences/graphql/mutations/updateCommsPreferences";
import { useSession } from "@foundation/shared/client/lib/auth";
import { useKrakenMutation } from "@krakentech/blueprint-api/client";
import { useQueryClient } from "@tanstack/react-query";

export const useUpdateCommsPreferences = () => {
const queryClient = useQueryClient();
const {
data: { sub },
} = useSession();

return useKrakenMutation({
document: updateCommsPreferences,
onSuccess({ updateCommsPreferences }) {
queryClient.setQueryData(["comms-preferences", sub], {
viewer: {
preferences: updateCommsPreferences?.commsPreferences,
},
});
},
});
};

So: GraphQL in graphql/, usage in client/hooks/.

3. Resolvers and mocks

The feature can include graphql/resolvers/ for mock GraphQL resolvers and, if needed, client/hooks/mocks/ for MSW handlers in tests and Storybook. When you lift the feature, copy these and wire them into the new app's test or mock set-up.

4. Components and pages

  • client/components/ – e.g. EditCommsPreferencesForm
  • client/pages/ – e.g. CommsPreferencesPage

These import the feature's hooks and, if needed, shared or app-level components. They do not define GraphQL.

For more on adding new features and hooks, see Creating a new query hook and Development overview.