La Élume Beauté

On this page
La Élume Beauté is a beauty brand, and I built the frontend of its online store, most of it in the first half of 2026. It's a React 19 app on Vite, with TanStack Router for the pages and TanStack Query for everything it fetches. It talks to the store's backend through a client generated from the backend's OpenAPI spec: a file that describes every endpoint of an API and the shape of what each one returns..
This page covers the big decisions. The chapters go through the store roughly the way a shopper meets it: the homepage, the language switcher, the cart and checkout, the account screens, and the member pages behind them. The last one is about the generated client underneath it all.
Why not Next.js?
It's a fair question, since most of my other projects here are Next.js. My honest answer is that I didn't want to deal with the Next.js caching shenanigans. So the store is a A site that loads one HTML page once, then draws every screen with JavaScript in the browser. with no server rendering at all.
Every call to the backend goes through one hand-written fetch function underneath the generated client. It adds the sign-in token and the shopper's language to each request, and if the backend ever answers with a The status code a server sends back when it won't accept your sign-in, for example because it has expired., it clears the token on the spot:
export const apiFetch = async (url: string, options: RequestInit = {}) => {
const headers = new Headers(options.headers);
const token = getToken();
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
headers.set("Accept-Language", getLanguage()); // "en", "id" or "zh"
const response = await fetch(url, { ...options, headers });
if (response.status === 401) {
clearToken(); // the session has expired, so sign out right here
}
if (!response.ok) {
throw response;
}
return response;
};
Server data lives in TanStack Query. The only Zustand stores are the guest cart and the chosen language, and both are saved to Storage in your browser that keeps small bits of data, like a cart, even after you close the tab. with its persist middleware.
The components are built on Base UI, which replaced Radix. Base UI ships as one package, which I find cleaner and easier to maintain. Radix had looked like one package too, radix-ui, but removing it took 60 @radix-ui/* packages out of the The file that lists every package the app installs, down to the exact version..
It always starts in localStorage
A lot of these stories start in the same place:
- The guest cart had to be merged out of localStorage.
- The token in localStorage had to be made visible to React.
- A saved cart holding prices as strings was behind the price that broke shipping rates.
Chapters
- 1Pages the CMS can't breakA homepage the brand edits itself, checked on the way in so a half-finished section can't break it.
- 2Switching language mid-requestThree languages from one set of queries, and the request that finished in the wrong one.
- 3The cart that follows youA guest cart that survives signing in, and the road from cart to paid.
- 4Proving it's youEmail or phone in one field, a header that wouldn't sign out, and codes over WhatsApp.
- 5Loyalty without made-up numbersBuilding a loyalty page around the parts its backend didn't have yet, without a single made-up number.
- 6A team tree you can zoomA tree drawn with flexbox, and level labels that follow the zoom without React.
- 7Living with a generated clientMaking regeneration boring when the backend's spec never sits still.