Primate

Quickstart

The easiest way to get started with Primate is to run it.

Node Deno Bun
$ npx primate

This boots an app from the current directory and serves any route files under routes. Let's create one:

TypeScript JavaScriptroutes/index.tsroutes/index.js
import route from "primate/route";

// handle GET requests at this route
route.get(() => "Hello, world!");

Your app now greets you at http://localhost:6161.

Requests to / are handled by an index file. Primate uses filesystem-based routing.

Create config

To customize your app, create a config directory and a config file:

TypeScript JavaScriptconfig/app.tsconfig/app.js
import config from "primate/config";

export default config({
  // config comes here
});

Add frontend

Install the Primate package for your frontend, plus the frontend itself:

React Angular Vue Svelte Solid
$ npm install @primate/react react react-dom

All @primate/* packages are officially supported and versioned with Primate.

Add the frontend to your config:

React Angular Vue Svelte Solidconfig/app.tsconfig/app.tsconfig/app.tsconfig/app.tsconfig/app.ts
import react from "@primate/react";
import config from "primate/config";

export default config({
  modules: [react()],
});

Then create a components directory and a component — here, a simple counter:

React Angular Vue Svelte Solidcomponents/Counter.jsxcomponents/Counter.component.tscomponents/Counter.vuecomponents/Counter.sveltecomponents/Counter.jsx
import { useState } from "react";

export default function Counter(props) {
  const [counter, setCounter] = useState(props.start);

  return (
    <div style={{ textAlign: "center", marginTop: "2rem" }}>
      <h2>Counter Example</h2>
      <div>
        <button onClick={() => setCounter(counter - 1)}>-</button>
        <span style={{ margin: "0 1rem" }}>{counter}</span>
        <button onClick={() => setCounter(counter + 1)}>+</button>
      </div>
    </div>
  );
}

Primate supports many frontends; the quickstart only shows a few.

Serve the component from a route with the view handler:

React Angular Vue Svelte Solidroutes/index.tsroutes/index.tsroutes/index.tsroutes/index.tsroutes/index.ts
import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("Counter.jsx", { start: 10 }));

Switch backend

By default, routes run in TypeScript/JavaScript. To add another backend, install its package:

Go Python Ruby Grain
$ npm install @primate/go

For some backends (e.g. Go), you'll need a compiler to produce Wasm. See the backend docs for setup details.

Then add it to your config:

Go Python Ruby Grainconfig/app.tsconfig/app.tsconfig/app.tsconfig/app.ts
import go from "@primate/go";
import config from "primate/config";

export default config({
  modules: [go()],
});

And create a route:

TypeScript JavaScript Go Python Ruby Grainroutes/index.tsroutes/index.jsroutes/index.goroutes/index.pyroutes/index.rbroutes/index.gr
import route from "primate/route";
import view from "primate/view";

route.get(() => view("Counter.jsx", { start: 10 }));

Backends and frontends are fully interchangeable — combine them freely.

Add database

To persist data across reloads, install a database package plus pema for validation:

SQLite PostgreSQL MySQL MongoDB SurrealDB
$ npm install pema @primate/sqlite

Configure your database:

SQLite PostgreSQL MySQL MongoDB SurrealDBconfig/database.tsconfig/database.tsconfig/database.tsconfig/database.tsconfig/database.ts
import sqlite from "@primate/sqlite";

export default sqlite({
  database: "/tmp/app.db",
});

Except for SQLite, you need a running database server.

Create a store for your counter:

TypeScript JavaScriptstores/Counter.tsstores/Counter.ts
import i8 from "pema/i8";
import primary from "pema/primary";
import store from "primate/store";

export default store({
  id: primary,
  value: i8.range(-20, 20),
});

Validation is applied before writes — databases handle structure and types, Primate adds higher-level rules.

Update your component to sync with the backend:

React Angular Vue Svelte Solidcomponents/Counter.tsxcomponents/Counter.component.tscomponents/Counter.vuecomponents/Counter.sveltecomponents/Counter.tsx
import validate from "@primate/react/validate";

interface Props { counter: number; id: string };

export default function Counter(props: Props) {
  const counter = validate<number>(props.counter).post(
    `/counter?id=${props.id}`,
  );

  return (
    <div style={{ marginTop: "2rem", textAlign: "center" }}>
      <h2>Counter Example</h2>
      <div>
        <button onClick={() => counter.update(n => n - 1)}
          disabled={counter.loading}>
          -
        </button>
        <span style={{ margin: "0 1rem" }}>{counter.value}</span>
        <button onClick={() => counter.update(n => n + 1)}
          disabled={counter.loading}>
          +
        </button>
      </div>
      {counter.error && <p style={{ color: "red" }}>{counter.error.message}</p>}
    </div>);
}

And wire it together with a route:

TypeScript JavaScriptroutes/index.tsroutes/index.js
import Counter from "#store/Counter";
import pema from "pema";
import number from "pema/number";
import string from "pema/string";
import response from "primate/response";
import route from "primate/route";

await Counter.schema.create();

route.get(async () => {
  const counters = await Counter.find({});

  const counter = counters.length === 0
    ? await Counter.insert({ value: 10 })
    : counters[0];

  return response.view("Counter.jsx", counter);
});

route.post(async request => {
  // validate that an id was provided
  // request.query.get() will throw if id is missing
  const id = string.parse(request.query.get("id"));

  // validate body as a number, coercing from string first
  const body = request.body.fields(pema({ value: number }).coerce);

  // update the value in the database
  await Counter.update({ id }, { value: body.value });

  // 204 no response
  return null;
});

Use pema's coerce to turn web inputs into typed values before validation.

Wrap up

With just a few files, you now have an app that can:

Next steps

At this point, you've built a minimal but complete app. From here, you can scaffold a project, explore real examples, or dive deeper into the docs — and start building production-ready apps.