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, andpreviewscripts 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 insrc/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 intoAppModule 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.
| Aspect | Backend module / microservice | Spa module |
|---|---|---|
| Runs | On the server, as part of the API | In the browser |
| Registration | Registered into AppModule / SharedModule | Not registered |
| tsconfig aliases | Added to the root tsconfig paths | Not added |
| Build & serve | Bundled with the app | Built by Vite, served to the browser |
| Stack | Controllers, services, repositories | React, 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 folder | What 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. |
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
- Structure — the full layout of a spa module.
- Features — vertical slices and
spa:feature:create. - Routing — file-based routing with TanStack Router.
- Data fetching — server state with TanStack Query.
- Internationalization — translation hooks in a spa.
- Create —
spa:createin full. - Remove —
spa:removein full. - Design System — the design module a spa composes.
- Modules — how backend modules differ.