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/, orcomponents/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 root –
graphql/queriesandgraphql/mutationsare the single place for that feature's operations. - Client code is under
client/– Hooks, components, pages, and mocks are grouped underclient/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
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.EditCommsPreferencesFormclient/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.