Primate 0.35: Server hot reload, standalone builds, server-client type safety
Today we're announcing the availability of the Primate 0.35 preview release. This release introduces server hot reload for all backends — including those compiled to WebAssembly—, standalone production builds, server-client type safety, and a completely redesigned build system that eliminates previous complexity.
Redesigned build system
Primate 0.35 fundamentally changes how the framework handles builds. Instead of
copying files to the build directory and running them from there, Primate now
bundles your server code into a single file, enabling hot reloading during
development and producing standalone executables for production.
Benefits of the new architecture
The new build system provides several key advantages:
- Faster development — Hot reload works for all backend code, including code compiled to WebAssembly
- Simpler deployment — Production builds are self-contained with no external dependencies
- Better performance — Bundled code eliminates filesystem overhead during runtime
- Cleaner code — Direct esbuild integration replaces abstraction layers
Customizable build directory
You can now specify a different build output location using the --dir flag.
$ npx primate build --dir=out$ deno run -A npm:primate build --dir=out$ bunx --bun primate build --dir=outYou can also serve from a different directory.
$ npx primate serve --dir=out$ deno run -A npm:primate serve --dir=out$ bunx --bun primate serve --dir=outThis is particularly useful for deployment pipelines or when integrating with other tools.
build directory from earlier versions may
trigger a warning that it does not contain a valid previous build. Remove the
build directory manually to proceed.Server hot reload
Primate now supports server hot reloading when modifying backend code during development. Whether your routes are written in TypeScript, JavaScript, or any supported WebAssembly language, Primate will reload your changes instantly without restarting the runtime process.
During development, the generated server bundle only contains routes and stores, keeping it lightweight. Each change triggers a fast regeneration and reimport cycle, providing a development experience on par with client-side hot reload.
How it works
When you modify a route file:
- Primate detects the change via filesystem watching
- The server bundle is regenerated (typically under 100ms)
- The new bundle is imported without restarting the runtime process
- Your browser automatically refreshes to reflect the changes
This works seamlessly across all supported backend languages, including Go, Python, and Ruby.
Server-client type safety
One of the most significant improvements in 0.35 is full type safety between
your server routes and client views. Previously, when using response.view,
you had to pass view names as strings, making it difficult to ensure props had
the correct shape and type.
In 0.35, you can now directly import view components and use them with
response.view. TypeScript will verify that the props you pass match what the
component expects.
Type-safe views
Before (0.34):
// routes/user.ts
import route from "primate/route";
route.get(request => {
// string-based, no type checking
return response.view("UserProfile.tsx", {
name: "Bob",
age: 30,
// typos or wrong types go unnoticed
});
});After (0.35):
// routes/user.ts
import route from "primate/route";
import UserProfile from "#view/UserProfile";
route.get(request => {
// direct import, fully type-checked
return response.view(UserProfile, {
name: "Bob",
age: 30,
// TypeScript will error if props don't match component signature
});
});
Full type inference
The view component's prop types are automatically inferred:
// views/UserProfile.tsx
export default function UserProfile({ name, age }: {
name: string;
age: number;
}) {
return (
<div>
<h1>{name}</h1>
<p>Age: {age}</p>
</div>
);
}Now in your route:
import UserProfile from "#view/UserProfile";
route.get(() => {
return response.view(UserProfile, {
name: "Bob",
age: "thirty", // TypeScript error: Type 'string' is not assignable
}); // to type 'number'
});
Benefits
- Catch errors early — Type mismatches are caught during development, not at runtime
- Better IDE support — Full autocomplete for prop names and types
- Refactoring safety — Changing a component's props automatically updates all usage sites
- Self-documenting code — Component signatures serve as documentation
@plsp/svelte) and enabling it in your tsconfig.json.
Server-client type safety is currently supported for JSX frontends (React, Solid, Voby) and Svelte. Support for Vue and Angular will be added in a future release.
Standalone production builds
Production builds now generate a single build/server.js file that bundles all
dependencies, static assets, routes, stores, and views. This file can be
executed directly:
$ node build/server.js$ deno build/server.js$ bun build/server.jsNo node_modules, no npm install, no build step required on the production
server. Everything needed to run your application is contained in one file.
What gets bundled
- All npm dependencies
- Your application code (routes, stores, views)
- Static assets (images, fonts, etc.) as base64-encoded data
- Configuration files
- Compiled WebAssembly modules
node_modules due to their
native dependencies. Full standalone support for these languages is planned for
later versions.Enhanced plugin system
The previous config.build option has been replaced with a more powerful
plugin API. You can now register esbuild plugins directly for both client and
server builds:
import type BuildApp from "primate/BuildApp";
import type { Plugin } from "esbuild";
import Module from "@primate/core/Module";
export default new class extends Module {
name: "custom-module",
build(app: BuildApp, next) {
// add a client-side plugin
app.plugin("client", {
name: "custom/client/plugin",
setup(build) {
// plugin code
},
});
// add a server-side plugin
app.plugin("server", {
name: "custom/server/plugin",
setup(build) {
// plugin code
},
});
return next(app);
}
}This provides full control over the build process for both frontend and backend compilation.
Breaking changes
tsconfig.json paths no longer required
Primate no longer provides specific paths or include configurations in
tsconfig.json. You can now organize your project however you prefer. We still
recommend extending primate/tsconfig for sensible defaults:
{
"extends": "primate/tsconfig",
"compilerOptions": {
"baseUrl": "${configDir}",
"paths": {
"#view/*": ["views/*"],
"#store/*": ["stores/*"],
"#config/*": ["config/*"]
}
},
"include": [
"config",
"routes",
"views",
"stores"
]
}
Session configuration simplified
Sessions now use Primate stores for both persistence and validation, eliminating the need for separate managers and schemas.
Create a session store:
// stores/Session.ts
import p from "pema";
import store from "primate/store";
export default store({
id: p.primary,
session_id: p.string.uuid(),
user_id: p.number,
last_active: p.date,
});Configure sessions:
// config/session.ts
import Session from "#store/Session";
import session from "primate/session/config";
export default session({
store: Session,
cookie: { name: "session" }
});Use in routes:
import session from "#session";
import route from "primate/route";
route.get(() => {
if (!session.exists) {
session.create({ user_id: 42 });
}
const data = session.get();
return `User ${data.user_id} last active at ${data.last_active}`;
});
Removal of the bundle config option
The bundle config option has been removed.
Primate now automatically detects all packages that should be included in the
build, making manual bundle configuration unnecessary.
Store#schema renamed to Store#collection
The Store instance property schema has been renamed to collection to more
accurately reflect its purpose. Update any references accordingly.
What's next
Check out our issue tracker for upcoming 0.36 features.
Fin
If you like Primate, consider joining our Discord server or starring us on GitHub.