The Universal Web Framework
Frontend, backend, runtime — Primate lets you pick the tools you love
and combine them however you like, without
lock-ins or rewrites.
Use any frontend.
Primate supports every major frontend. Split your app across frameworks, or migrate gradually without rewrites.
React Angular Vue Sveltecomponents/Index.jsxcomponents/Index.component.tscomponents/Index.vuecomponents/Index.svelte
export default function Index({ posts }) {
return <>
<h1>All posts</h1>
{posts.map(post => (
<h2 key={post.id}>
<a href={`/post/${post.id}`}>
{post.title}
</a>
</h2>
))}
</>;
};
import { Component, Input } from "@angular/core";
@Component({
template: `
<h1>All posts</h1>
<div *ngFor="let post of posts">
<h2>
<a href="/post/{{post.id}}">
{{post.title}}
</a>
</h2>
</div>
`,
})
export default class Index {
@Input() posts = [];
}
<template>
<h1>All posts</h1>
<div v-for="post in posts">
<h2>
<a :href="`/post/${post.id}`">
{{post.title}}
</a>
</h2>
</div>
</template>
<script>
export let posts;
</script>
<h1>All posts</h1>
{#each posts as post}
<h2>
<a href="/post/{post.id}">
{post.title}
</a>
</h2>
{/each}
Combine many backends.
Write routes in JS/TS by default. Extend with WebAssembly backends and mix them freely. New backends are added based on demand.
TypeScript Go Python Rubyroutes/index.tsroutes/index.goroutes/index.pyroutes/index.rb
import response from "primate/response";
import route from "primate/route";
const posts = [{
id: 1,
title: "First post",
}];
route.get(() => response.view("Index.jsx", { posts }));
package main
import (
"github.com/primate-run/go/core"
"github.com/primate-run/go/response"
"github.com/primate-run/go/route"
)
var _ = route.Get(func(_ route.Request) any {
posts := core.Array[core.Dict]{core.Dict{
"id": 1,
"title": "First post",
}}
return response.View("Index.jsx", core.Dict{
"posts": posts,
})
})
from primate import Route, Response
@Route.get
def get(request):
posts = [
{
"id": 1,
"title": "First post",
}
]
return Response.view("Index.jsx", {"posts": posts})
require 'primate/route'
require 'primate/response'
Route.get do |request|
posts = [{
id: 1,
title: "First post",
}]
Response.view("ndex.jsx", { posts: posts })
end
Choose your runtime.
Consistent APIs on Node, Deno, and Bun — native execution paths under the hood, no runtime boilerplate.
Node Deno Bun
$ npx primate
$ deno run -A npm:primate
$ bun --bun x primate
Extensive ecosystem.
Official modules for real apps: databases, sessions, auth, i18n, native builds — with more coming.
Store Route Componentstores/Post.tsroutes/posts.tscomponents/Posts.jsx
import date from "pema/date";
import primary from "pema/primary";
import string from "pema/string";
import store from "primate/store";
export default store({
id: primary,
title: string.max(50),
body: string,
created: date.default(() => new Date()),
});
import Post from "#store/Post";
import response from "primate/response";
import route from "primate/route";
route.get(async () => {
const posts = await Post.find({}, { limit: 10 });
return response.view("Posts.jsx", { posts });
});
import t from "@primate/react/i18n";
import locale from "@primate/react/locale";
export default function({ username }) {
return <>
<h3>{t("switch-language")}</h3>
<div>
<a onClick={() => locale.set("en-US")}>
{t("English")}
</a>
</div>
<div>
<a onClick={() => locale.set("de-DE")}>
{t("German")}
</a>
</div>
</>;
}
Get productive.
Short guides for common tasks. Browse topics and jump into the docs.