Wardn Hub
MCP ServersSkillsCategoriesAPI docsSubmit server
Submit server
Wardn HubTrusted MCP server directory.

Registry

  • MCP Servers
  • Skills
  • Categories

Resources

  • API docs
  • Score method

Contribute

  • Submit server
  • Advertise
© 2026 Wardn Hub
Wardn Hub
MCP ServersSkillsCategoriesAPI docsSubmit server
Submit server
skills/sickn33/agentic-awesome-skills/frontend-architecture

frontend-architecture

1
sickn33/agentic-awesome-skills·Architecture & Design·Audit passed·Snapshot dbfbbc6f2d3e
Installs
1

Summary

A portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion rules....

SKILL.md

Frontend Architecture (portable, module-based)

When to Use

Use this skill when you need a portable, framework-agnostic architecture style for any React or React Native frontend. Organizes apps into feature modules with page/screen directories, a strict server-state vs UI-state split, barrel-only cross-module imports, co-located styles, and clear component-promotion rules....

Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others. This skill describes a structure and a set of rules, not a component library, a state library, or a visual style. It is deliberately global: the same module/page/state model maps onto Next.js (App Router), React + Vite (SPA), Remix, and Expo / React Native, and it works with any state-management and styling stack.

The goal: a codebase where any contributor can instantly answer three questions — "where does this code live?", "what is allowed to import what?", and "is this server state or UI state?" — without asking anyone. The structure makes the answers obvious.


0. The five core ideas

  1. Feature modules own their world. Each feature is a self-contained modules/{feature}/ folder with its own pages, components, hooks, state, types, and a single public barrel.
  2. Pages/screens are directories, not files. A route is a folder that co-locates its component, its styles, and the components/hooks used only by it.
  3. State is split by origin. Server data lives in a query/cache layer. UI/client state lives in a store. They never overlap — regardless of which libraries you pick.
  4. Imports cross boundaries only through barrels. Reaching into another module's internals is forbidden; you import from and nothing deeper.
@/modules/{feature}
  • Code is promoted, not pre-placed. It starts as local as possible and moves outward only when a second consumer appears.
  • Everything below is the mechanical application of these five ideas. None of it is tied to a specific library — pick your stack in Sections 4 and 6.


    1. Directory layout

    The shape is identical across frameworks; only the routing layer on top differs (see Section 7).

    src/
    ├── app/ or routes/ or navigation/   ← framework routing layer (thin — see §7)
    ├── modules/                         ← feature modules (the heart of the app)
    │   └── {feature}/
    │       ├── index.ts                 ← PUBLIC BARREL — the only cross-module entry point
    │       ├── README.md                ← what this module owns, its routes, its data deps
    │       ├── components/              ← components reused by 2+ pages IN THIS MODULE
    │       ├── pages/                   ← page/screen directories (one per route)
    │       │   └── {page}/
    │       │       ├── {page}.tsx               ← the page/screen component
    │       │       ├── {page}.styles.ts         ← ALL styling for this page
    │       │       ├── index.ts                 ← re-exports the page component
    │       │       ├── components/              ← components used ONLY by this page
    │       │       ├── hooks/                   ← hooks used ONLY by this page
    │       │       ├── constants/
    │       │       └── README.md                ← route, params, permissions, data deps
    │       ├── hooks/                   ← data hooks (query/mutation) + module hooks
    │       ├── stores/                  ← UI/client state store(s) — never server data
    │       ├── services/                ← data-access (API calls) for this feature
    │       ├── utils/                   ← pure module utilities (co-located *.test.ts)
    │       ├── constants/
    │       └── types/                   ← module request/response + view-model types
    └── shared/                          ← cross-module building blocks
        ├── components/                  ← components used by 2+ MODULES
        ├── hooks/                       ← cross-cutting hooks
        ├── api-client/                  ← one typed client; the only place that talks to the network
        ├── store/                       ← root store wiring (if your state lib needs one — see §4)
        ├── utils/                       ← formatters, cn()/clsx, helpers
        ├── constants/
        └── types/
    

    Every folder that can be empty at scaffold time keeps a .gitkeep so the structure is visible from day one.


    2. Feature modules

    A module is a vertical slice of the product (e.g. auth, billing, dashboard, settings). It contains everything that feature needs and exposes a deliberately small surface.

    2.1 The barrel (index.ts) is the contract

    modules/{feature}/index.ts is the only thing other modules and the routing layer may import from. It re-exports:

    • Page/screen components the router mounts.
    • Data hooks other features legitimately need.
    • The store hook/slice and its public types.
    • Shared constants / types other features depend on.
    // CORRECT — consume the public surface
    import { InvoiceListPage, useInvoiceList } from "@/modules/invoice";
    
    // WRONG — reaching into internals couples you to private structure
    import { InvoiceListPage } from "@/modules/invoice/pages/invoice-list/invoice-list";
    

    Keep the barrel curated. If something isn't exported, it's private by design. Group exports with short comments (pages, hooks, store, types) — future readers use the barrel as the module's API docs.

    2.2 One module = one bounded context

    Don't create utils modules or components modules. Modules map to product capabilities, not to technical layers. Technical building blocks live in shared/.

    2.3 Module README

    Each module's README.md states: what it owns, which routes render its pages, its data dependencies (which endpoints/hooks), and any cross-module rules. This is the first thing a new contributor reads.


    3. Pages/screens as directories

    A page is a route the router mounts (a "screen" in React Native). It is always a folder, never a loose file — even when it starts as a single component. This keeps growth in place: when the page needs a sub-component or a hook, there is already a home for it.

    pages/{page}/
    ├── {page}.tsx          ← the page/screen component
    ├── {page}.styles.ts    ← every style for this page (no inline styles — see §5)
    ├── index.ts            ← export { PageComponent } from "./{page}"
    ├── components/         ← used ONLY by this page
    ├── hooks/              ← used ONLY by this page
    ├── constants/
    └── README.md           ← route, params, permissions, data deps
    

    The page README is short and high-signal: route path, expected params, required permissions/auth, and the hooks it depends on. It is the contract between the page and the rest of the app.

    Why folders from the start: a page that begins as one file inevitably grows a sub-row component, a derived-totals hook, a styles file. If the page is a file, those land in arbitrary places. If the page is a folder, they have an obvious home and the diff stays readable.


    4. State: split by origin (non-negotiable, library-agnostic)

    Two kinds of state, two homes. Mixing them is the most common architectural failure this skill exists to prevent. The split is mandatory; the libraries are your choice.

    State kindExamplesLives in
    Server statefetched entities, lists, aggregates — anything the API ownsa query/cache layer (e.g. TanStack Query, RTK Query, SWR, Apollo)
    UI / client stateopen dialogs, table filters/sort, wizard step, draft being typed, preview togglesa client store (e.g. Zustand, Redux Toolkit, MobX, Jotai, Valtio, or React Context)

    4.1 Hard rules (independent of library)

    • Never mirror server responses into the client store. No copying fetched entities into Zustand/Redux/MobX. The query/cache layer is the single source of truth for server data.
    • Never fetch inside components. Components read server data from a data hook and UI state from a store selector. They don't call the network client directly.
    • Never drive continuous values through re-render state. Scroll progress, pointer position, drag offset — use refs / animation values, not render state (it re-renders the tree every frame).
    • One store boundary per module. Whatever library you use, give each module one cohesive store unit (a Zustand hook, a Redux slice, a MobX class, a Jotai atom group) accessed via the module barrel. Components subscribe to the smallest slice they need to avoid needless re-renders.

    4.2 Choosing a client-state library — same shape, different syntax

    Pick one per project and stay consistent. Each maps onto "one store unit per module" cleanly. Note the I interface-naming convention: state interfaces are prefixed with I (e.g. IFeatureUiState).

    Zustand — modules/{feature}/stores/{feature}.store.ts

    import { create } from "zustand";
    
    export interface IFeatureUiState {
      isPreviewOpen: boolean;
      filter: string;
      togglePreview: () => void;
      setFilter: (filter: string) => void;
      reset: () => void;
    }
    
    const INITIAL_STATE = { isPreviewOpen: false, filter: "" } as const;
    
    export const useFeatureUiStore = create<IFeatureUiState>()((set) => ({
      ...INITIAL_STATE,
      togglePreview: () => set((s) => ({ isPreviewOpen: !s.isPreviewOpen })),
      setFilter: (filter) => set({ filter }),
      reset: () => set({ ...INITIAL_STATE }),
    }));
    

    Redux Toolkit — modules/{feature}/stores/{feature}.slice.ts (registered in shared/store/)

    import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
    
    export interface IFeatureUiState {
      isPreviewOpen: boolean;
      filter: string;
    }
    
    const initialState: IFeatureUiState = { isPreviewOpen: false, filter: "" };
    
    export const featureUiSlice = createSlice({
      name: "featureUi",
      initialState,
      reducers: {
        togglePreview: (s) => {
          s.isPreviewOpen = !s.isPreviewOpen;
        },
        setFilter: (s, action: PayloadAction<string>) => {
          s.filter = action.payload;
        },
        reset: () => initialState,
      },
    });
    

    MobX — modules/{feature}/stores/{feature}.store.ts

    import { makeAutoObservable } from "mobx";
    
    export interface IFeatureUiState {
      isPreviewOpen: boolean;
      filter: string;
    }
    
    export class FeatureUiStore implements IFeatureUiState {
      isPreviewOpen = false;
      filter = "";
      constructor() {
        makeAutoObservable(this);
      }
      togglePreview = () => {
        this.isPreviewOpen = !this.isPreviewOpen;
      };
      setFilter = (filter: string) => {
        this.filter = filter;
      };
      reset = () => {
        this.isPreviewOpen = false;
        this.filter = "";
      };
    }
    

    Jotai — modules/{feature}/stores/{feature}.atoms.ts

    import { atom } from "jotai";
    export const isPreviewOpenAtom = atom(false);
    export const filterAtom = atom("");
    

    Whichever you choose, keep the rules in §4.1 constant. The skill cares that server and UI state are separated and that each module owns one store unit — not which library draws the box.

    4.3 Data layer (server state)

    All network access goes through one typed client in shared/api-client/. Modules wrap it in query/mutation hooks and a key factory so caches and invalidation stay consistent.

    // modules/invoice/hooks/invoiceKeys.ts — hierarchical key factory (TanStack Query style)
    export const invoiceKeys = {
      all: ["invoices"] as const,
      lists: () => [...invoiceKeys.all, "list"] as const,
      list: (params: IListParams) => [...invoiceKeys.lists(), params] as const,
      details: () => [...invoiceKeys.all, "detail"] as const,
      detail: (id: string) => [...invoiceKeys.details(), id] as const,
    } as const;
    

    Invalidating lists() refreshes every filtered page; detail(id) targets one entity. (RTK Query/SWR/Apollo express the same idea with tags/keys.) Components never write raw fetch() — they call useInvoiceList() / useCreateInvoice().


    5. Styling: co-located, no inline styles (styling-library agnostic)

    Keep styling out of JSX and out of the component body. Each page or component has a co-located styles file. The rule is constant; the syntax follows your styling stack.

    • Tailwind (web): {name}.styles.ts exports named class strings composed with cn() (clsx + tailwind-merge); variants via cva. JSX references styles.header.
    • CSS Modules / vanilla-extract: a co-located {name}.module.css / {name}.css.ts; JSX references styles.header.
    • styled-components / Emotion: a co-located {name}.styles.ts exporting styled components.
    • Tamagui (web + native): a co-located {name}.styles.ts exporting styled(...) components or a createStyledContext / useStyle token set; reference Tamagui tokens ($background, $space.4) — never hardcoded values inline. Tamagui is the recommended choice when you target both web and React Native from one codebase.
    • React Native StyleSheet / Nativewind: a co-located {name}.styles.ts exporting StyleSheet.create({...}) (or Nativewind classnames). JSX references styles.header.
    // invoice-list.styles.ts (Tailwind example)
    export const invoiceListStyles = {
      page: "flex flex-col gap-8",
      header: "flex flex-col gap-1.5",
      title: "text-3xl font-semibold tracking-tight",
    } as const;
    
    // invoice-list.styles.ts (Tamagui example — works on web AND native)
    import { styled, YStack, Text } from "tamagui";
    
    export const InvoiceListPage = styled(YStack, { flex: 1, gap: "$8" });
    export const InvoiceListHeader = styled(YStack, { gap: "$1.5" });
    export const InvoiceListTitle = styled(Text, {
      fontSize: "$8",
      fontWeight: "600",
    });
    

    No inline style={{...}} literals in the component body, on any stack. Why: styling drifts and duplicates when it lives inline. A co-located styles file gives one place to audit spacing rhythm, theme correctness, and responsive behavior per surface. Document non-obvious choices (accent locks, breakpoints) in comments there.

    This skill does not dictate the visual design — pair it with a design/component skill for that. It dictates only where styling lives.


    6. Naming conventions

    Consistent naming makes the structure self-describing.

    • Interfaces are prefixed with I — IFeatureUiState, IInvoiceListParams, IUserProfile. Type aliases (unions, mapped types, primitives) are not prefixed (type SortDirection = "asc" | "desc").
    • Components: PascalCase files and exports — InvoiceListPage.tsx, LineItemRow.tsx.
    • Pages/screens: kebab-case directories, the component file matches — pages/invoice-list/invoice-list.tsx.
    • Hooks: useCamelCase — useInvoiceList, useFeatureUiStore.
    • Stores: {feature}.store.ts (Zustand/MobX), {feature}.slice.ts (Redux), {feature}.atoms.ts (Jotai). Hook is use{Feature}{Purpose}Store.
    • Styles: {name}.styles.ts co-located with its owner.
    • Constants: SCREAMING_SNAKE_CASE values; kebab-case or camelCase files.
    • Barrels: always index.ts.

    7. Framework adapters

    The module/page/state model is constant. Only the thin routing layer on top changes. Pages always live in modules/; the routing layer just mounts them.

    7.1 Next.js (App Router)

    • src/app/ holds route segments and route groups ((marketing), (app), (public)) for layout/auth boundaries. Route files are thin: import a page component from a module barrel and render it.
    • Default to Server Components; mark interactive leaves "use client". Providers (query client, store, theme) live in a "use client" boundary.
    // app/(app)/invoices/page.tsx — thin route file
    import { InvoiceListPage } from "@/modules/invoice";
    export default function Page() {
      return <InvoiceListPage />;
    }
    

    7.2 React + Vite (SPA)

    • A src/routes/ (or single router.tsx) declares the route table (React Router / TanStack Router) and maps paths to module page components. Everything is client-side. Wrap the tree once with the query-client and store/theme providers at the app root.

    7.3 Remix

    • Route modules in app/routes/ stay thin and re-export/mount module page components; loaders/actions delegate to the module's services/. Module boundaries are unchanged.

    7.4 Expo / React Native

    • Routing is Expo Router (file-based, in app/) or React Navigation (navigation/). Route/screen files are thin and import screen components from module barrels.
    • "Pages" are "screens" — same directory pattern: pages/{screen}/{screen}.tsx + {screen}.styles.ts.
    • Query layer + client store run unchanged (TanStack Query, Zustand, Redux, MobX, Jotai all work in RN). The typed api-client is shared logic and works as-is.
    • Styling uses Tamagui (recommended for shared web+native), StyleSheet, or Nativewind. Keep module logic DOM-free.
    // app/invoices/index.tsx (Expo Router) — thin screen file
    import { InvoiceListScreen } from "@/modules/invoice";
    export default InvoiceListScreen;
    

    7.5 Sharing across web + native

    If you target both web and Expo, push framework-free code (types, validators, formatters, the API client contract) into a shared package consumed by both apps, and prefer Tamagui for components that must render on both. Module boundaries stay the same on both sides.


    8. Conventions checklist (enforce in review)

    • New feature → new modules/{feature}/ with index.ts + README.md, not files scattered into shared/.
    • New route → a page/screen directory ({page}.tsx + {page}.styles.ts + index.ts + README.md), not a loose file.
    • Cross-module imports go through the barrel (@/modules/{feature}) — no deep internal paths.
    • Server data is in the query/cache layer; UI state is in the module store; neither leaks into the other (whatever libraries are chosen).
    • No fetch() in components — only typed data hooks built on the shared client.
    • No inline styles — co-located {name}.styles.ts (Tailwind/CSS Modules/Tamagui/StyleSheet/styled-components).
    • Components/hooks/utils placed at the narrowest scope; promoted only when a 2nd consumer appears.
    • One store unit per module, accessed via the barrel, with selectors and a reset.
    • Interfaces use the I prefix; components/hooks/files follow §6.
    • Query keys/tags come from a per-module factory; invalidation is hierarchical.
    • Routing files are thin — they mount module pages and own only layout/auth boundaries.
    • Module/page READMEs updated when routes, params, or data deps change.

    9. Component promotion (start local, move outward)

    A component is born in the narrowest scope that uses it and is promoted only when a second consumer appears. Never pre-place a component "because it might be reused."

    A component used by…Lives inImported as
    Only one pagepages/{page}/components/relative path within the page
    2+ pages in one modulemodules/{feature}/components/@/modules/{feature} (via barrel)
    2+ modulesshared/components/@/shared/...
    2+ apps / reposa published design-system packagethe package name

    The same ladder applies to hooks, utils, and constants: local → module → shared → package. Promotion is a deliberate move (update the import sites), not a guess made up front.


    10. How to apply this skill

    Scaffolding a new app: create src/modules/, src/shared/, and the framework routing layer (§7). Add the shared api-client, the query layer, and your chosen client-store provider. Drop a store template into the first module.

    Adding a feature: create modules/{feature}/ with the full subfolder set (pages/ components/ hooks/ stores/ services/ utils/ constants/ types/), a curated index.ts, and a README.md. Build the first screen as a page directory.

    Deciding where code goes: ask "who consumes this?" → narrowest scope wins (§9). Ask "where did this data come from?" → server = query layer, UI = store (§4).

    Reviewing structure: run the checklist in §8. The most valuable catches are state-origin leaks (server data in the client store) and deep cross-module imports (bypassing the barrel) — both erode the architecture fastest.


    Publishing / installing this skill

    This skill follows the Anthropic SKILL.md format and is portable across agents. To make it installable and discoverable (e.g. on skills.sh / npx skills):

    1. Put this folder under a skills/ directory in a public GitHub repo (path like skills/frontend-architecture/SKILL.md).
    2. Keep the frontmatter name and a high-signal description (above) — that description is what discovery indexes match against.
    3. Install from any project with: npx skills add <org>/<repo> --skill "frontend-architecture".
    4. Non-SKILL.md agents can be pointed here from AGENTS.md / CLAUDE.md; Kiro can mirror it as a steering file.

    Limitations

    • Use this skill only when the task clearly matches its upstream source and local project context.
    • Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
    • Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.

    Related skills

    kubernetes-deploymentdeployment-engineergit-workflow-and-versioningkubernetes-architectpre-release-review