24 Frontend App Structure
Purpose of this page
Define a frontend structure that:
- enables premium app-like UX (fast navigation, responsive search),
- supports offline-friendly caching (IndexedDB snapshot),
- stays maintainable as screens grow,
- and keeps us portable (no dependence on backend rendering or BaaS client SDKs).
- We're using Next.js + TypeScript + Tailwind + shadcn/ui because:
Next.js is the most proven web app framework for production scale,
React ecosystem gives us mature UI patterns,
and shadcn/ui gives us accessible components we own in our repo (full control).
High-level frontend architecture
Core idea
The frontend is a PWA client that talks to our NestJS API. It keeps a local snapshot of inventory in IndexedDB to make:
- search instant,
- cellar/offline browsing possible,
- and UI feel “native”.
Separation of concerns
UI components: reusable and dumb (no direct API calls)
Data layer: API client + cache + sync logic
Screens/routes: compose components + call data layer
This separation is what keeps a Next.js codebase scalable.
Route map (MVP)
We structure routes to match screens in Page 06.
Auth & onboarding
/login /register /onboarding
Main app (authenticated)
/dashboard → Dashboard (Search-first home) /locations /locations/[id] → Location Detail /items/[id] → Item Detail /items/new → Add Item /items/[id]/edit → Edit Item /items/[id]/move → Move picker (or modal route)
Settings
/settings /settings/import → CSV bulk import wizard (feature-gated: PRO+) /settings/team → Team member management (feature-gated: TEAM) /settings/subscription → Plan management
Why routes vs heavy modal-only navigation
Routes:
- preserve browser history (Back button works),
- allow deep linking,
- and improve debugging/testing.
- We can still show certain screens as modals while maintaining real routes.
Layout structure
App shell
AppLayout for authenticated routes:
- top bar (contextual)
- content
- bottom tab nav
Auth layout
minimal centered layout for login/register
Why
A consistent shell makes PWA feel like a native app and reduces repeated code.
State and data management (how we keep it simple but scalable)
Data sources
- API (source of truth)
- IndexedDB cache (offline + speed)
Data layer modules
We'll implement a small set of modules rather than adopting a heavy global state framework prematurely.
Recommended structure
1frontend/
2src/
3app/ (Next.js app router)
4components/
5features/
Why this over Redux from day 1
Redux is great but adds ceremony. For our MVP:
- most state is server-derived,
- we can use local component state + lightweight caching,
- then add a state library later if needed.
API client conventions
lib/api/client.ts wraps fetch includes:
- credentials: "include" (cookies)
- base URL
- standardized error handling (Page 16 error shape)
Why
Consistency prevents “random fetch calls everywhere” and makes debugging far easier.
IndexedDB (Dexie) caching plan (MVP)
What we cache
locations (flat) items (flat) optionally precomputed breadcrumbs for items
When we write cache
after successful sync calls (online) after writes (create/edit/move/delete) succeed Only for accounts whose current plan includes offline mode. If a user loses that entitlement, the client should clear the local inventory snapshot instead of continuing to serve stale offline data.
When we read cache
on app start to populate UI immediately when offline 6. auth/ 7. items/ 8. locations/ 9. search/
10lib/
11 api/
12 client.ts
13 endpoints.ts
14 types.ts
15 cache/
16 db.ts (Dexie schema)
17 sync.ts
18 auth/
19 session.ts
20 utils/
21 debounce.ts
Why this improves UX
instant search even with slow network usable in cellars without signal fast UI = higher trust
Search architecture (critical)
Online-first, offline fallback
If online:
- call /search?q=... for canonical results
- update local cache optionally
- If offline:
- if the signed-in plan includes offline mode, search locally in IndexedDB snapshot
- otherwise show an offline-unavailable recovery state instead of cached results
- show “Offline — results may be stale” banner
Why not always local search
Local search is fast, but online search can incorporate:
- server ranking improvements,
- future features (synonyms, typo tolerance),
- and ensures consistency across devices.
UI composition rules (how screens are built)
Screen components
Each route is a screen that composes reusable components. Example:
Dashboard (Search-first home)
SearchBar
FavoritesList (from API)
SearchResultsList (ItemRow list)
EmptyState
Component ownership
components/ holds generic UI (Button wrappers, BreadcrumbPath, EmptyState) features/* holds domain components (ItemRow, LocationRow, TagInput, PhotoPicker, FavoritesList)
Why
This prevents a “components soup” and keeps domain logic grouped.
Authentication gating in the frontend
Strategy
Client-side route protection:
If /me fails → redirect to /login
Use a shared useSession() hook that:
- fetches /me once,
- caches user state,
- triggers sync on login.
Why
Simple and effective for PWA clients with cookie auth.
Error and loading patterns (must-have)
Skeletons for lists
Inline error banners with Retry
Toast for “Saved/Moved/Deleted”
No full-screen spinners for core browsing/search
Why
These patterns directly affect perceived speed and trust.
PWA structure & caching
manifest + icons service worker caches the app shell data caching handled by IndexedDB, not service worker caching of API
Why
Service worker caching of authenticated API responses can create stale-data and security complexity. IndexedDB gives explicit control.
Decisions (locked for MVP)
Next.js routes map 1:1 to major screens (Back button friendly)
Shared AppLayout with bottom tab navigation
Central API client wrapper (credentials included, standard errors)
Dexie IndexedDB cache for locations/items snapshot
Search: online-first with offline fallback to local snapshot