Primate Logo Primate

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.

If you're new to Primate, we recommend reading the Quickstart page to get started.

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:

Customizable build directory

You can now specify a different build output location using the --dir flag.

npm Deno Bun
$ npx primate build --dir=out

You can also serve from a different directory.

npm Deno Bun
$ npx primate serve --dir=out

This is particularly useful for deployment pipelines or when integrating with other tools.

When upgrading to 0.35, an existing 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:

  1. Primate detects the change via filesystem watching
  2. The server bundle is regenerated (typically under 100ms)
  3. The new bundle is imported without restarting the runtime process
  4. 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

Svelte server-client type safety requires installing the Primate Svelte TypeScript plugin (@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 Deno Bun
$ node build/server.js

No node_modules, no npm install, no build step required on the production server. Everything needed to run your application is contained in one file.

Always serve the build using the same runtime that generated it. Primate currently does not support cross-runtime builds.

What gets bundled

Python and Ruby backends currently still require 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.