Primate 0.42: Virtual route groups, structured forms and production diagnostics
Today we're announcing the availability of the Primate 0.42 preview release.
Virtual route groups
Primate 0.42 adds URL-transparent route directories. Wrap a directory name in parentheses to organize routes and scope special files without adding a segment to the public URL.
routes/
(account)/
+layout.ts
projects.ts
settings.ts
[organization]/
(admin)/
+hook.ts
members.tsroutes/(account)/projects.ts serves /projects, and
routes/[organization]/(admin)/members.ts serves
/:organization/members. Groups can be nested or placed below static and
dynamic segments.
The directories remain part of the filesystem hierarchy. An (account) layout
frames only that group's pages, while an (admin) hook applies only inside its
subtree. This makes it possible to share layout, hook and error behavior among
selected siblings without inventing a URL prefix.
Because groups disappear from the URL, Primate checks their complete route patterns for collisions at startup. Static, dynamic, optional and rest overlaps fail with every conflicting source path instead of depending on directory traversal order.
Explicit lazy bundle seams
For client-rendered pages, a group is also a lazy bundle seam. Its collocated pages, layouts, CSS and route-related dependencies are emitted behind a group entrypoint in the partition's shared esbuild graph. The application shell and framework runtime can remain common.
Direct responses preload the active group. On the first client transition into another group, Primate prepares its JavaScript and CSS before disposing the current page. A failed load leaves the current URL, DOM and styles intact; later navigation can retry. Once loaded, navigation within that group returns to the usual JSON-only path.
Nested routes belong to their deepest group, while ancestor layouts remain
dependencies. Components in the top-level views directory stay in the
application bundle because arbitrary route handlers can select them at runtime.
See Virtual route groups for naming, collision and chunking details.
Structured client forms
client.form can now serialize a browser form into the nested JSON type
declared by its route. Pass serialize to turn FormData into objects, arrays,
booleans or another application-specific structure.
import preferences from "@/routes/account/preferences";
import client from "@primate/react/client";
const form = client.form(preferences.post, {
serialize(data) {
return {
notifications: {
enabled: data.has("notifications[enabled]"),
channels: data.getAll("notifications[channels]").map(String),
},
};
},
});The serializer receives successful controls, repeated values and the initiating submit button. Its result is checked against the imported route's body input type and then continues through the normal submitting, result, redirect and validation lifecycle.
Nested Pema issues map back to bracketed field names. For example,
/notifications/channels/0 maps to
notifications[channels][0]. Field paths recursively check object keys and
numeric array indices; an explicit Record<string, T> permits dynamic keys.
Nested initial values follow the same path.
For URL-encoded requests, request.body.formEntries() preserves repeated names
and submission order. The platform request.original.formData() path likewise
keeps repeated controls. Use these for checkbox groups and multi-selects while
keeping form() for a last-value record.
See Client forms and Request bodies.
Declarative document heads for Marko
Marko applications can now declare document metadata with the same component ownership model used by Primate's React and Solid integrations.
import { Head } from "@primate/marko/tags";
<Head>
<title>${input.article.title}</title>
<meta name="description" content=input.article.summary>
<link rel="canonical" href=input.article.url>
</Head>During SSR, Primate collects title, meta, link, style, script and
base elements into the document head. Hydration adopts that state without
duplicates. Client navigation replaces metadata owned by outgoing pages and
layouts while leaving application-template elements untouched. If no active
Head supplies a title, the template title is restored.
The implementation validates child elements and title ownership on both server and client. Script and style raw text is preserved even when it contains text that resembles a template closing tag.
See Marko Head.
Navigation completion subscriptions
Application-wide browser integrations can subscribe after Primate has updated both the destination URL and DOM.
import client from "primate/client";
const unsubscribe = client.onNavigation(({ source, url }) => {
analytics.page(url, { source });
});The source is "navigate", "form", "history" or "refresh". Successful
link navigation, programmatic navigation, enhanced forms, Back, Forward and
refresh each notify exactly once. Failed, superseded, shallow, hash-only and
full-document navigation do not notify.
Completion is fenced throughout asynchronous asset preparation, frontend disposal and mounting. If another operation takes ownership during one of those steps, the stale operation cannot update history or emit a completion signal. Use the returned function to unsubscribe, and continue using component lifecycle hooks for behavior local to a view.
Observable request failures
Modules can register onError to observe unexpected request failures before
Primate renders the application's error route.
setup({ onError }) {
onError(async ({ frames, phase, request, route }) => {
const incident = await report({ frames, phase, route });
request.set("incident", incident.id);
});
}Observers receive the original cause, request facade, matched route template,
processing phase and mapped application frames. They run inside request context,
so an observer can attach a safe incident ID for +error without exposing the
exception to the client.
Production builds now include a linked private build/server.js.map. Primate
uses it to map bundled stack positions back to application-relative source
locations before invoking observers. Source content is not embedded, and the
map is neither a client asset nor served by Primate.
The error context distinguishes matching, handlers, layouts, rendering, error handlers and handle middleware. Observer failures are isolated, logged and do not replace the original response.
Expected response aborts
Use response.abort when deeply nested application code needs to stop request
processing with an intentional response.
import response from "primate/response";
function requireUser(request) {
if (!request.session.exists) {
response.abort({ status: 401, body: "Sign in required" });
}
}An abort can carry status, headers, body or an existing Response. It bypasses
error logging, onError and +error, while still unwinding route hooks and
layouts. This keeps expected control flow separate from operational failures.
See Response aborts.
Independent database pools
PostgreSQL, MySQL and MongoDB now expose common pool controls for maximum
connections, connection timeout and idle timeout. They also support fork()
for work that needs its own capacity and lifecycle.
const dedicated = db.fork({ pool: { max: 2 } });
try {
await runLongTransaction(dedicated);
} finally {
await dedicated.close();
}A fork privately inherits connection settings while owning an independent pool. Closing it does not close the parent, and closing the parent does not close its forks. The shared adapter contract also gives database packages a consistent way to verify this isolation.
See Connection pools.
Production and deployment controls
Two new application settings provide more control over production servers and bundles.
http.timeout configures the in-flight HTTP idle timeout on runtimes that
expose one. Development keeps it disabled for debugging and live reload. This
is an inactivity timeout rather than an overall request deadline, so long-lived
responses can remain open by sending heartbeats.
export default config({
http: { timeout: 255_000 },
});build.external keeps selected packages out of the production server bundle.
Use it for native addons, adjacent WebAssembly or package-owned data files that
must remain in node_modules at runtime.
export default config({
build: { external: ["satori", "@resvg/resvg-js"] },
});See App configuration.
Upgrade notes
Client groups are now partitions
The independently built client bundle feature introduced in 0.41 is now named
client.partitions. Rename the client.groups key; client.routes remains the
URL-prefix mapping.
export default config({
client: {
partitions: {
public: "/app/public/",
account: "/app/account/",
},
routes: {
"/": "public",
"/account": "account",
},
},
});Partitions remain independently built asset-isolation boundaries. Virtual route groups are traversable chunk seams inside a partition; the two concepts are intentionally different.
Migration autoapply has been removed
db.migrations.autoapply, deprecated in 0.41, is removed. Leaving the property
configured produces an error with migration instructions. Apply migrations
explicitly in development:
npx primate migrate:applyFor deployments, apply the exact artifact emitted by the build before starting its server:
node build/migrate.js apply
node build/server.jsThis keeps schema changes visible to release jobs and prevents multiple server instances from racing startup migrations.
Dependency refresh
The workspace's external dependencies have been refreshed across frontend,
database, build and test packages. The @rcompat/io 0.13 migration adopts its
explicit process result model so failed compiler and tooling commands continue
to stop Primate operations rather than being mistaken for successful output.
Bug fixes
Client navigation and forms
- Link enhancement now respects
defaultPrevented, modifier keys, downloads and non-default targets. This was also released for the 0.41 line. - Redirecting links and non-OK/non-Primate responses reliably fall back to the browser instead of leaving the current page unchanged. This was also backported to 0.41.
- Enhanced GET forms use native query semantics, omit a request body and honor submit-button action, method and encoding overrides.
- Frontend page responses with validation statuses such as 400 and 422 render through client navigation, while ordinary JSON remains an API response.
- Redirect chains preserve their final pathname, query and fragment for links and forms, then scroll after the destination mounts.
- Repeated URL-encoded and multipart controls survive parsing in their original
order through
formEntries()and the platformFormDatainterface. - Failed or superseded asset and frontend transitions leave history and completion subscriptions under the active operation's ownership.
Routing and rendering
- A specific dynamic directory can coexist with a rest-route fallback. Primate continues deeper through the specific branch and falls back to the rest route only when needed.
- A layout or error route's method-specific handler takes precedence, then its
gethandler frames views and errors returned from POST and other methods. - Explicit SSR now works in development even when a frontend also has a client runtime; CSR-only applications remain client-only.
- Template placeholder cleanup removes only actual unused placeholders, so
ordinary percent text between unrelated
%characters is preserved. - Intentional aborts raised while matching, handling, laying out, rendering or running an error route no longer become spurious 500 observations.
Build, migration, session and runtime reliability
primate buildand other CLI commands now exit nonzero when their operation fails, allowing CI and container builds to stop at the original error.- A failed development migration apply closes its database before returning a failure status, rather than leaving the process alive on an open pool.
- The default in-memory session store now persists created and updated session data for the lifetime of the app instead of silently discarding it.
- UUID generation works on the declared Node 24 baseline without relying on
newer
Uint8Arrayhex helpers. Compatibility patches were also released for 0.41. - Type-only imports and declarations were corrected in Core, JSONDB and Go so their published ESM output does not request erased TypeScript symbols at runtime.
Changes also shipped in 0.41 patches
The 0.41 maintenance line received the fixes that were safe to release without waiting for 0.42: native link interception, redirect/error navigation fallback, independent database pools, structured error observation with private source maps, and Node 24 UUID compatibility. Applications already on the latest 0.41 patch have those fixes; the new APIs and breaking configuration changes above remain part of 0.42.
Fin
If you like Primate, consider joining our Discord server or starring us on GitHub.