I had a plan from last week. Build AdminLayout, set up routing, get the first component working.
Then I remembered: to show anything meaningful in the browser, I needed data. And the only endpoint that doesn’t require a logged-in user is the public weekly menu.
So I started there instead.
Starting with the public menu#
GET /api/v1/menus/current returns the active weekly menu — no token required. It was the perfect first API call: real data, no auth complexity, immediate feedback in the browser.
Getting it working was straightforward. useEffect fires on mount, calls the service, updates state, and the component renders the result. The pattern clicked fast.
But as soon as the menu was rendering, I wanted to make it look like something. So I started building components. A card for each day. A component for the week selector. An input field. A button. A badge for each dish.
And then I looked at my src/components folder.
The chaos#
src/components/
├── AllergenList/
│ ├── AllergenList.jsx
│ └── AllergenList.module.css
├── AllergenPill/
│ ├── AllergenPill.jsx
│ └── AllergenPill.module.css
├── Button/
│ ├── Button.jsx
│ └── Button.module.css
├── Card/
│ ├── Card.jsx
│ └── Card.module.css
├── DayColumn/
│ ├── DayColumn.jsx
│ └── DayColumn.module.css
├── Input/
│ ├── Input.jsx
│ └── Input.module.css
├── LoginForm/
│ ├── LoginForm.jsx
│ └── LoginForm.module.css
├── LogContainer/
│ ├── LogContainer.jsx
│ └── LogContainer.module.css
├── Logo/
│ ├── Logo.jsx
│ └── Logo.module.css
├── MenuGrid/
│ ├── MenuGrid.jsx
│ └── MenuGrid.module.css
├── MenuSlotCard/
│ ├── MenuSlotCard.jsx
│ └── MenuSlotCard.module.css
├── Notification/
│ ├── Notification.jsx
│ └── Notification.module.css
├── PublicMenuPage/
│ ├── PublicMenuPage.jsx
│ └── PublicMenuPage.module.css
└── RegisterForm/
├── RegisterForm.jsx
└── RegisterForm.module.cssAll in the same folder. All at the same level. No structure, no grouping, no way to tell which ones were specific to the menu feature and which ones were truly shared across the app.
This was after just five days. The project had barely started and I was already dreading opening the folder.
I knew from the backend that this kind of pain is a signal. It means the structure doesn’t reflect the problem. So before building more, I stopped and looked at what my options actually were.
Researching the right structure#
The two main approaches I found were:
Type-based — group by what something is: all pages together, all services together, all components together. Simple to explain, but it means a feature is spread across four folders.
Feature-based — group by what something does: everything related to suggestions lives in features/suggestions, everything related to menus lives in features/menus. A feature is self-contained.
For a project the size of MiseOS, the feature-based approach was clearly better. And when I looked at it alongside my API contract, it mapped almost perfectly:
API: /api/v1/dish-suggestions → features/suggestions/
API: /api/v1/menus → features/menus/
API: /api/v1/ingredient-requests → features/ingredient-requests/Each feature owns its pages, components, and service. Shared things — Button, Input, Card — go in src/components/ui. The question where does this go? has a clear answer now, and it matches how the backend is already organized.
The refactor#
I moved things in small steps and kept the project compiling the entire time.
That’s actually one of the habits that saved me the most time this week: always keep the project in a state that compiles. Make one change, check the browser, then make the next. When something breaks, you know exactly what caused it because you only changed one thing.
The browser console was open the whole time. Not as a last resort when something went wrong — as the default. If something doesn’t render as expected, the answer is usually in the console before you’ve even started guessing.
It sounds obvious, but it changes how you work. You stop writing 50 lines and then wondering why it doesn’t work. You write a few lines, look at the browser, adjust, continue.
After the refactor, the structure looked like this:
src/
├── components/
│ └── ui/ ← Button, Input, Card, Logo,
│ LogoContainer, Notification
├── features/
│ ├── menus/
│ │ ├── components/ ← AllergenList, AllergenPill,
│ │ │ DayColumn, MenuGrid, MenuSlotCard
│ │ ├── pages/ ← PublicMenuPage
│ │ └── services/ ← menuService
│ └── auth/
│ ├── components/ ← LoginForm, RegisterForm
│ ├── pages/ ← LoginPage, RegisterPage
│ └── services/ ← authService
└── layouts/Building the login and register forms#
With the structure sorted, I could build properly. Login and register were next — and they were my first real controlled forms in React.
A controlled form means React owns every input value. Every keystroke calls handleChange, which updates state, which re-renders the input. The input never holds its own state.
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};Using name from the event target means one handler works for all fields. Adding a new field to the form doesn’t require touching the handler at all.
LoginForm is minimal on purpose — it calls onSubmit(credentials) and nothing else. No navigation, no API calls, no error display beyond what the parent sends down. The page handles what happens after.
RegisterForm is more involved. Passwords need to match, fields can’t be empty, and if the API rejects the email, that error should appear on the email field specifically — not just as a generic message at the top. That required thinking about two separate error states: field-level validation errors and API-level form errors.
const { errors: validationErrors, hasErrors } = validateRegistration(formData);
if (hasErrors) {
setErrors(validationErrors);
return;
}
const { confirmPassword, ...newUser } = formData;
await onSubmit(newUser);confirmPassword gets stripped before the API call — the backend has no concept of it and would reject the request. Destructuring to exclude a key is exactly the right tool here.
One small thing that made a big difference: clearing a field’s error as soon as the user starts correcting it. Without this, submitting an invalid form and then typing would leave stale errors on screen until the next submission.
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }));
}Reflections#
I didn’t plan to refactor the folder structure this week. I planned to build features. But the pain of a messy components folder after two days made it obvious that working on top of a bad structure would cost more than fixing it early.
The refactor was worth it — not because the structure is clever, but because it removed a constant friction. I stopped thinking about where files go and started thinking about what the code should do.
The feature-based structure also turned out to mirror the API contract almost exactly, which made sense in retrospect. The backend was already organized around the same domain concepts. The frontend just needed to follow the same map.
Next step#
The structure is solid, the forms are working, and the public menu is rendering real data. Next up: authentication. Getting login working end to end — token storage, context, protected routes, and what happens when a session expires.
This is part 12 of the MiseOS development log. Follow along as I build a management tool for professional kitchens, one commit at a time.
