Primate
Expressive, minimal and extensible framework for JavaScript.
Getting started
Create a route in routes/hello.js
export default router => {
router.get("/", () => "Hello, world!");
};
Add {"type": "module"}
to your package.json
and run npx -y primate@latest
.
Table of Contents
Serving content
Create a file in routes
that exports a default function.
Plain text
export default router => {
// strings will be served as plain text
router.get("/user", () => "Donald");
};
JSON
import {File} from "runtime-compat/filesystem";
export default router => {
// any proper JavaScript object will be served as JSON
router.get("/users", () => [
{name: "Donald"},
{name: "Ryan"},
]);
// load from a file and serve as JSON
router.get("/users-from-file", () => File.json("users.json"));
};
Streams
import {File} from "runtime-compat/filesystem";
export default router => {
// `File` implements `readable`, which is a `ReadableStream`
router.get("/users", () => new File("users.json"));
};
Response
import {Response} from "runtime-compat/http";
export default router => {
// use a generic response instance for a custom response status
router.get("/create", () => new Response("created!", {status: 201}));
};
Routing
Routes map requests to responses. They are loaded from routes
.
Basic
export default router => {
// accessing /site/login will serve `Hello, world!` as plain text
router.get("/site/login", () => "Hello, world!");
};
The request object
export default router => {
// accessing /site/login will serve `["site", "login"]` as JSON
router.get("/site/login", request => request.path);
};
Accessing the request body
For requests containing a body, Primate will attempt to parse the body according
to the content type sent along the request. Currently supported are
application/x-www-form-urlencoded
(typically for form submission) and
application/json
.
export default router => {
router.post("/site/login", ({body}) => `submitted user: ${body.username}`);
};
Regular expressions
export default router => {
// accessing /user/view/1234 will serve `1234` as plain text
// accessing /user/view/abcd will show a 404 error
router.get("/user/view/([0-9])+", request => request[2]);
};
Named groups
export default router => {
// named groups are mapped to properties of `request.named`
// accessing /user/view/1234 will serve `1234` as plain text
router.get("/user/view/(?<_id>[0-9])+", ({named}) => named._id);
};
Aliasing
export default router => {
// will replace `"_id"` in any path with `"([0-9])+"`
router.alias("_id", "([0-9])+");
// equivalent to `router.get("/user/view/([0-9])+", ...)`
// will return id if matched, 404 otherwise
router.get("/user/view/_id", request => request.path[2]);
// can be combined with named groups
router.alias("_name", "(?<name>[a-z])+");
// will return name if matched, 404 otherwise
router.get("/user/view/_name", request => request.named.name);
};
Sharing logic across requests
import html from "@primate/html";
import redirect from "@primate/redirect";
export default router => {
// declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
router.alias("edit-user", "/user/edit/([0-9])+");
// pass user instead of request to all verbs with this route
router.map("edit-user", () => ({name: "Donald"}));
// show user edit form
router.get("edit-user", user => html`<user-edit user="${user}" />`);
// verify form and save, or show errors
router.post("edit-user", async user => await user.save()
? redirect`/users`
: html`<user-edit user="${user}" />`);
};
Extensions
There are two ways to extend Primate's core functionality. Handlers are used per route to serve new types of content not supported by core. Modules extend an app's entire scope.
Handlers and modules listed here are officially developed and supported by Primate.
Handlers
HTML
Serve HTML tagged templates. This handler reads HTML component files from
components
.
Create an HTML component in components/user-index.html
<div for="${users}">
User ${name}.
Email ${email}.
</div>
Create a route in route/user.js
and serve the component in your route
import html from "@primate/html";
export default router => {
// the HTML tagged template handler loads a component from the `components`
// directory and serves it as HTML, passing any given data as attributes
router.get("/users", () => {
const users = [
{name: "Donald", email: "donald@the.duck"},
{name: "Joe", email: "joe@was.absent"},
];
return html`<user-index users="${users}" />`;
});
};
Redirect
Redirect the request.
Create a route in route/user.js
import redirect from "@primate/html";
export default router => {
// redirect the request
router.get("/user", () => redirect`/users`);
};
HTMX
Serve HTML tagged templates with HTMX support. This handler reads HTML component
files from components
.
Create an HTML component in components/user-index.html
<div for="${users}" hx-get="/other-users" hx-swap="outerHTML">
User ${name}.
Email ${email}.
</div>
Create a route in route/user.js
and serve the component in your route
import {default as htmx, partial} from "@primate/htmx";
export default router => {
// the HTML tagged template handler loads a component from the `components`
// directory and serves it as HTML, passing any given data as attributes
router.get("/users", () => {
const users = [
{name: "Donald", email: "donald@the.duck"},
{name: "Joe", email: "joe@was.absent"},
];
return htmx`<user-index users="${users}" />`;
});
// this is the same as above, with support for partial rendering (without
// index.html)
router.get("/other-users", () => {
const users = [
{name: "Other Donald", email: "donald@the.goose"},
{name: "Other Joe", email: "joe@was.around"},
];
return partial`<user-index users="${users}" />`;
});
};
Modules
To add modules, create a primate.config.js
configuration file in your
project's root. This file should export a default object with the property
modules
used for extending your app.
export default {
modules: [],
};
Data persistance
Add data persistance in the form of ORM backed up by various drivers.
Import and initialize this module in your configuration file
import domains from "@primate/domains";
export default {
modules: [domains()],
};
A domain represents a collection in a store using the static fields
property
import {Domain} from "@primate/domains";
// A basic domain with two properies
export default class User extends Domain {
static fields = {
// a user's name is a string
name: String,
// a user's age is a number
age: Number,
};
}
Field types may also be specified as an array with additional predicates aside from the type
import {Domain} from "@primate/domains";
import House from "./House.js";
export default class User extends Domain {
static fields = {
// a user's name is a string unique across the user collection
name: [String, "unique"],
// a user's age is a positive integer
age: [Number, "integer", "positive"],
// a user's house has the foreign id of a house record and no two
// users may have the same house
house_id: [House, "unique"],
};
}
Resources
- Website: https://primatejs.com
- IRC: Join the
#primate
channel onirc.libera.chat
.
License
MIT