Skip to content

Micro-Frontends

A τjs system can contain several frontend applications without adding a browser-side shell or a module-federation runtime. Each application has its own renderer, client build and SSR build. Fastify selects the application at the HTTP route boundary.

That gives the model a few firm properties:

  • one response is rendered by one configured application and one renderer
  • each application produces its own client and SSR artefacts
  • application routes are ordinary Fastify routes
  • applications do not share a browser memory space across document navigations
  • shared code is bundled through normal imports, not loaded from another application at runtime

Every application declares its renderer, entry point and routes:

taujs.config.ts
import { reactRenderer } from "@taujs/react/renderer";
import { defineConfig } from "@taujs/server/config";
import { solidRenderer } from "@taujs/solid/renderer";
export default defineConfig({
apps: [
{
appId: "customer",
entryPoint: "customer",
renderer: reactRenderer({ project: "./tsconfig.json" }),
routes: [
{ path: "/app", attr: { render: "ssr" } },
{ path: "/app/*", attr: { render: "ssr" } },
],
},
{
appId: "admin",
entryPoint: "admin",
renderer: solidRenderer({ project: "./tsconfig.solid.json" }),
routes: [
{
path: "/admin",
attr: {
render: "ssr",
middleware: { auth: { roles: ["admin"] } },
},
},
{
path: "/admin/*",
attr: {
render: "ssr",
middleware: { auth: { roles: ["admin"] } },
},
},
],
},
],
});

The exact route owns the bare prefix. The terminal wildcard owns URLs below it. Fastify performs concrete route dispatch, then τjs applies the selected application’s data, policy and renderer contract.

Authentication metadata is application data for your Fastify authenticate decorator to enforce. See Authentication for that boundary.

The default layout keeps applications and framework-neutral shared code separate:

src/
├── client/
│ ├── customer/
│ │ ├── entry-client.tsx
│ │ ├── entry-server.tsx
│ │ └── App.tsx
│ └── admin/
│ ├── entry-client.tsx
│ ├── entry-server.tsx
│ └── App.tsx
├── server/
│ ├── index.ts
│ └── services/
└── shared/
├── contracts/
├── design-tokens/
└── utilities/
taujs.config.ts

This is a convention, not a workspace requirement. clientBaseDir, entry points and aliases can be configured when a repository needs another layout.

Your build script calls taujsBuild(). A normal project exposes that script through commands such as npm run build:client and npm run build:ssr.

Each application is built separately:

dist/
├── client/
│ ├── customer/
│ │ ├── assets/
│ │ └── manifest.json
│ └── admin/
│ ├── assets/
│ └── manifest.json
└── ssr/
├── customer/
│ └── server.js
└── admin/
└── server.js

Separate builds mean each application has its own import graph. Code imported by two applications may appear in both outputs. τjs does not create a cross-application shared chunk or a runtime that coordinates those chunks.

A build can target an appId or entryPoint:

Terminal window
node scripts/build.mjs --app admin
TAUJS_APPS=customer,admin node scripts/build.mjs

This supports per-application CI jobs when applications change at different rates. It is important to distinguish a selective build from an independently deployable release:

  • client builds clean dist/ before producing their selected outputs
  • the running server expects the client and SSR artefacts for every configured application it may serve
  • independent deployment therefore needs an artefact pipeline that assembles and publishes the required per-application outputs together

Without that assembly step, build and deployment remain coordinated. See Build & Deployment for the complete filter and output behaviour.

Undeclared URLs stay with the client router. A URL omitted from taujs.config.ts is not a server-owned request contract; once the application’s document is served, its client router can own that URL.

That document still needs an HTTP owner:

  • a τjs-created host provides the implicit SPA fallback document
  • a caller-owned Fastify host needs a declared terminal wildcard such as /app/* for τjs to own unmatched client URLs

See Request Contracts & Data and Host Ownership.

A link from /app/orders to /admin/users is a document navigation. The server selects a different application and the browser starts that application’s client runtime. In-memory state, active component trees, WebSockets and uncommitted UI state do not cross that boundary.

Use ordinary links for cross-application navigation:

<a href="/admin/users">Admin</a>

If a seam cuts through state that must remain alive, the seam is in the wrong place. Move the boundary rather than introducing a client orchestrator solely to preserve that state.

Making document navigation feel continuous

Section titled “Making document navigation feel continuous”

Full-document navigation does not require a white flash or a browser-side application shell. Platform features can progressively improve the transition while preserving the request boundary.

Enable navigation transitions in shared CSS and give stable chrome the same view-transition-name in each application:

@view-transition {
navigation: auto;
}
.site-header {
view-transition-name: site-header;
}

Supporting browsers animate the document change. Other browsers perform the same normal navigation without the animation. Keep names unique within each rendered document.

Links between known application boundaries can be prefetched or prerendered with browser speculation rules. Treat this as progressive enhancement: the server route and streamed response remain the correct path when speculation is unavailable or cancelled.

Do not use either feature to imply state continuity. They improve delivery and presentation, not the ownership model.

These are different concerns:

ConcernBehaviour
Shared source moduleImported and built into each consuming application
Browser store instancePrivate to the current application document
Non-sensitive preferenceMay be restored from browser storage
Session or authoritative stateRe-established from the server on the next request
Critical or deferred route dataOwned by one response and one selected application

Read Dependency Management for import and bundle behaviour, and Shared State Management for safe persistence choices.

A separate application is useful when a URL area has a distinct renderer, delivery cadence, policy boundary or team ownership and can tolerate document navigation at the seam.

Keep routes in one application when they need continuous in-memory state, shared live connections or frequent transitions where a document boundary would be artificial. τjs supports several applications, but it does not require every organisational boundary to become one.