Skip to main content
A spa module is a Vite-built React single-page app that lives under modules/<name>/. Its <name>.yml declares type: "spa" and records the design module it consumes with a design: line; its package.json (named @module/<kebab>) ships dev, build, and preview Vite scripts on the module’s own free port (default 5000). The src/ tree is populated from the upstream skeleton-spa repo and follows a strict layering: routes stay thin, features own vertical slices, and shared/ holds the code two or more features have in common.

Directory layout

modules/<name>/                   # type: "spa"
  public/                         # Static files served verbatim — favicon, logos, robots.txt
  src/
    bootstrap/                    # Entry point and build wiring
      index.html                  #   HTML shell — mount node + script tag
      app.tsx                     #   Creates the router + React root and mounts it
      reportWebVitals.ts          #   Web-vitals (LCP, CLS, INP) collection hook
      routeTree.gen.ts            #   Auto-generated route tree — never edit
    routes/                       # File-based TanStack Router routes
      __root.tsx                  #   Root route: layout, error/not-found boundaries
      index.tsx                   #   Index (/) route — landing page
    features/                     # Vertical slices, one folder per domain feature
      <feature>/
        assets/                   #     Images/SVG/media used only by this feature
        components/               #     React components scoped to the feature
        hooks/                    #     The ONLY layer that talks to the backend
        layouts/                  #     Layout wrappers for this feature's pages
        services/                 #     Pure domain rules — never talks to backend
        store/                    #     Feature-local client state, not server data
        styles/                   #     CSS scoped to this feature
        translations/             #     i18n generated by translation:create
          translations.json
          use<Name>Translate.ts
        types/                    #     Feature domain types
        utils/                    #     Pure helpers within the feature
    shared/                       # Cross-feature code reused by >=2 features
      assets/  components/  hooks/  layouts/  services/  store/  styles/  types/  utils/

Module-level files

Two files at the module root describe the spa to the framework and to Vite.
FilePurpose
<kebab>.ymlModule manifest. Declares type: "spa" and a design: "<kebab>" line recording which design module this spa consumes.
package.jsonNamed @module/<kebab>. Exposes dev, build, and preview Vite scripts; the dev server runs on the module’s own free port (default 5000).
See Overview for how a spa fits into a workspace and pairs with a design module.

Top-level folders

The src/ tree is organized as four layers — entry wiring, routes, feature slices, and shared code — plus a public/ directory served as-is.
FolderWhat lives hereNotes
public/Static files copied verbatim to the web root — favicon, logos, robots.txt.No bundling or hashing; reference by absolute path (/favicon.svg).
src/bootstrap/Entry point and build wiring: index.html, app.tsx, reportWebVitals.ts, routeTree.gen.ts.Rarely edited by hand once scaffolded. routeTree.gen.ts is generated — never edit it.
src/routes/File-based TanStack Router routes; each file maps to a URL path.Keep thin — delegate UI to features, data to services/hooks.
src/features/Vertical slices, one folder per domain feature.A feature owns its full stack; do not import another feature’s internals — promote shared code to shared/.
src/shared/Cross-feature code reused by two or more features; same sub-layout as a feature.The only place features import common code from.

Inside bootstrap/

FileRole
index.htmlThe HTML shell — a single document with the mount node and script tag. Edit it for top-level <head> tags.
app.tsxCreates the router and the React root and mounts into the shell. Wire global providers (theme, query client) here.
reportWebVitals.tsWeb-vitals (LCP, CLS, INP) collection hook.
routeTree.gen.tsThe route tree auto-generated from routes/. Tooling output — never edit by hand.

Inside routes/

FileRole
__root.tsxRoot route: app-wide layout, error and not-found boundaries, shared chrome.
index.tsxThe index (/) route — the landing page.

Feature and shared sub-layers

Every features/<feature>/ folder and the shared/ folder share the same sub-layout. The layering is what keeps a spa maintainable: each sub-layer has one job, and the boundaries are enforced by convention.
Sub-layerPurposeLayering rule
assets/Images, SVG, and media used by the feature.Local to the feature; promote to shared/assets/ only when reused.
components/React components scoped to the feature.Render UI; pull data from hooks/, never fetch directly.
hooks/Data fetching, API calls, and local UI state.The ONLY layer allowed to talk to the backend.
layouts/Layout wrappers for the feature’s pages.Compose components/; hold no domain logic.
services/Feature business logic — pure domain rules.Never touches the backend; called by hooks and components.
store/Feature-local client state.Client state only, not server data — server data belongs in hooks/.
styles/CSS scoped to the feature.Keep feature-specific; shared styles go in shared/styles/.
translations/i18n generated by translation:create — a translations.json and a use<Name>Translate.ts hook.Generated; see Internationalization.
types/Feature domain types.Shared types move up to shared/types/.
utils/Pure helpers within the feature.No side effects, no backend access.
The two rules to internalize: hooks are the only layer that talks to the backend, and services hold pure domain logic and never touch the backend. Keeping that split clean is what lets you test domain rules in isolation and swap data sources without rewriting your features. See Data fetching for how hooks call the backend.

The shared/ folder

shared/ mirrors a feature’s sub-layout exactly — assets, components, hooks, layouts, services, store, styles, types, utils — and is the one place features import common code from. On create, the spa ensures every shared/ sub-layer exists, each tracked with a .gitkeep so the empty directories survive in git. shared/hooks/ ships useLang, scaffolded alongside the first translation hook. It reads the ?lang= query param (defaulting to en) so every feature resolves the active locale the same way.
A feature must never reach into another feature’s internals. When two or more features need the same component, hook, type, or helper, promote it to the matching shared/ sub-layer — that boundary is what stops a spa from collapsing into a tangle of cross-imports.
The design tokens and UI primitives a spa consumes come from its paired design module — see Design system structure for that side of the layout, and Features for how to build out a feature slice.