Release 0.31: new hot reload, projections and sorting in stores, new router

Today we're announcing the availability of the Primate 0.31 preview release. This release switches to fast hot reload using esbuild, adds projections and sorting to stores, and uses rcompat's new router, adding support for optional and rest path parameters.

If you're new to Primate, we recommend reading the Getting started page to get an idea of it.

Fast hot reload

Primate now uses esbuild as its built-in bundler via rcompat. This bundler utilizes esbuild's fast hot reloading, replacing the previous mechanism which used Node's file watching and was neither reliable nor fast.

Support for projections / sorting in stores

Primate stores have gained additional capabilities in this release.


It is now possible to add a projection to Store#find using a second parameter.

export default {
  get({ store: { User } }) {
    return User.find({}, ["name"]);

This will show a JSON array of objects from the user collection with only the name field.


It is now possible to influence the sorting order used in Store#find using a third parameter.

export default {
  get({ store: { User } }) {
    return User.find({}, ["name"], { sort: { name: "asc" } });

This will show a JSON array of objects from the user collection with only the name field, sorted by name ascendingly.

New filesystem router

This release now supports the whole breadth of type parameters similar to Next or Svelte using rcompat's new filesystem router.

Optional path parameters

Optional path parameters indicate a route which will be both matched with a path parameter and without it.

Double brackets in route filenames, as in user/[[action]].js, are equivalent to having two identical files, user.js and user/[action].js.

Optional parameters may only appear at the end of a route path and you can combine them with runtime types, like non-optional path parameters.

Rest path parameters

Rest path parameters are used to match subpaths at the end of a route path.

Brackets starting with three dots, as in user/[...action_tree].js, indicate a rest parameter. Unlike normal parameters, rest parameters match / as well and can be thus be used to construct subpaths. For example, in, docs/guide may be considered a subpath.

Rest parameters may only appear at the end of a route path. They may also be optional, that is, matching with and without the parameter, by using two brackets.

Quality of life improvements

HTMX integration improvements

Passing in props

The HTMX handler now supports passing in props, in JavaScript template string style. Consider the following route.

import { view } from "primate";

const posts = [{
  id: 1,
  title: "First post",

export default {
  get() {
    return view("post-index.htmx", { posts });

And the following HTMX component.

<h1>All posts</h1>
${ => `
    <a hx-get="/post/${}" href="/post/${}">

With that combination, a GET call to /htmx yields an HTMX-driven page with the posts handed in from the route.

This prop support extends to Primate's built-in html handler in the same fashion.

Partial rendering

Primate's view handler generally allows passing in { partial: true } as part of the third options parameter, which indicates the view component file to be rendered should not be embedded within the default app.html but delivered in bare form. This is great in case you use JavaScript to replace just a part of the page.

When using HTMX's DOM manipulation verbs (e.g. hx-get, hx-post, etc.), HTMX sends an hx-request header with the request. The view handler now, in the case of HTMX, checks whether this handler was sent along the request, and in such a case renders the component in partial mode.


If you're using Primate as a reverse proxy, you may now use the pass function on the request facade to pass a request wholesale to another backend.

You can do this generally in the handle hook.

export default {
  modules: {
    name: "proxy",
    handle(request, next) {
      // pass any requests whose path begins with /admin to another application
      // listening at port 6363
      if (request.url.pathname.beginsWith("admin")) {
        return request.pass("http://localhost:6363");

      // continue execution in this app otherwise
      return next(request);

Or specifically within a given route.

export default {
  get(request) {
    return request.pass("http://localhost:6363");

This passes only GET requests to /pass to another application at port 6363.

Passing a request per route usually makes sense in combination with disabled body parsing, which is now possible per route.

Disabling body parsing per route

In 0.30, we added the option to disable body parsing for the entire application. This release adds the option to do so per route.

export const body = {
  parse: false,

export default {
  get(request) {
    return request.pass("http://localhost:6363");

A local route body.parse export overrides the application-wide setting. This also means you could disable body parsing globally and then enable it for a specific route.

Migrating from 0.30

remove @primate/build

Primate now comes bundled with esbuild; remove any use of the deprecated @primate/build package; you also do not need to depend on esbuild yourself anymore.

Other changes

Consult the full changelog for a list of all relevant changes.

Next on the road

Some of the things we plan to tackle in the upcoming weeks are,

This list isn't exhaustive or binding. None, some or all of these features may be included in 0.32, and other features may be prioritized according to feedback.


If you like Primate, consider joining our channel #primate on

Otherwise, have a blast with the new version!