Skip to main content
A spa module holds a single-page application: the React app your users load in the browser. Unlike the other modules in your project, it carries no backend logic — no controllers, services, or repositories. It is a self-contained front end built with Vite, TanStack Router for file-based routing, and TanStack Query for data fetching, written in React and TypeScript. It lives under modules/<kebab-name>/ like any other module, and its <name>.yml declares type: "spa".

Why a spa module

  • Front-end, not backend. A spa module is the browser application — the UI users see and interact with. It does not register controllers or run on the server alongside your API.
  • Vite-powered. Each spa is a standalone Vite app with its own dev, build, and preview scripts and its own dev port, so you run and ship it independently.
  • TanStack Router + Query. Routing is file-based (routes are folders and files under src/routes/), and server state is fetched and cached with TanStack Query — no manual route table, no ad-hoc fetch glue.
  • Composes a design module. A spa builds its UI on top of a design module — the shared design system. The chosen design is recorded in the spa’s yml, so components and tokens come from one place.
  • Feature-sliced. Code is organized as vertical slices under src/features/, with cross-cutting code in src/shared/, so a domain’s routes, components, and hooks live together.

How it differs from a backend module

A regular module (or a microservice) is server-side: it registers into AppModule or SharedModule, exposes path aliases in the root tsconfig, and runs as part of the API. A spa module does none of that — it is a browser app that the API serves to the client.
AspectBackend module / microserviceSpa module
RunsOn the server, as part of the APIIn the browser
RegistrationRegistered into AppModule / SharedModuleNot registered
tsconfig aliasesAdded to the root tsconfig pathsNot added
Build & serveBundled with the appBuilt by Vite, served to the browser
StackControllers, services, repositoriesReact, Vite, TanStack Router/Query

How it works

ooneex spa:create scaffolds a base module, marks it type: "spa", and clones the upstream skeleton-spa repository into the module’s src/. It then installs the spa dependencies and assigns a free dev port — 5000 by default, or the next free port if that is taken — by adding dev, build, and preview Vite scripts to the module’s package.json. During creation you also pick the design module the spa composes (an existing type: "design" module or a new one), recorded as design: "<kebab>" in the spa’s yml.
Top-level folderWhat it holds
public/Static files served verbatim (favicons, images, robots.txt).
src/bootstrap/The entry point — index.html, app.tsx, and the generated routeTree.gen.ts.
src/routes/File-based routes; the folder and file layout is the URL structure.
src/features/Vertical slices, one per domain — routes, components, and hooks for a feature.
src/shared/Cross-feature code shared across the whole app.
A spa module’s tree therefore looks like this:
modules/dashboard/
├── dashboard.yml          # type: "spa", design: "<kebab>"
├── package.json           # dev/build/preview Vite scripts + dev port
└── src/                   # cloned from skeleton-spa
    ├── public/
    ├── bootstrap/
    │   ├── index.html
    │   ├── app.tsx
    │   └── routeTree.gen.ts
    ├── routes/
    ├── features/
    └── shared/

Working with a spa

Once created, you build the app out with the generators:
  • Add a feature slice with ooneex spa:feature:create — see Features.
  • Add a translation hook with ooneex translation:create — see Internationalization.
  • Remove a spa with ooneex spa:remove — see Remove.

Next steps