Primate, a cross-runtime framework
Primate is a full-stack cross-runtime Javascript framework (Node.js and Deno). It relieves you of dealing with repetitive, error-prone tasks and lets you concentrate on writing effective, expressive code.
Highlights
- Flexible HTTP routing, returning HTML, JSON or a custom handler
- Secure by default with HTTPS, hash-verified scripts and a strong CSP
- Built-in support for sessions with secure cookies
- Input verification using data domains
- Many different data store modules: In-Memory (built-in), File, JSON, MongoDB
- Easy modelling of
1:1
,1:n
andn:m
relationships - Minimally opinionated with sane, overrideable defaults
- Supports both Node.js and Deno
Getting started
Prepare
Lay out your app
$ mkdir -p primate-app/{routes,components,ssl} && cd primate-app
Create a route for /
// routes/site.js
import {router, html} from "primate";
router.get("/", () => html`<site-index date=${new Date()} />`);
Create a component for your route (in components/site-index.html
)
Today's date is <span data-value="date"></span>.
Generate SSL key/certificate
openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
Add an entry file
// app.js
import {app} from "primate";
app.run();
Run on Node.js
Create a start script and enable ES modules (in package.json
)
{
"scripts": {
"start": "node --experimental-json-modules app.js"
},
"type": "module"
}
Install Primate
$ npm install primate
Run app
$ npm start
Run on Deno
Create an import map file (import-map.json
)
{
"imports": {
"runtime-compat": "https://deno.land/x/runtime_compat/exports.js",
"primate": "https:/deno.land/x/primate/exports.js"
}
}
Run app
deno run --import-map=import-map.json app.js
You will typically need the allow-read
, allow-write
and allow-net
permissions.
Table of Contents
Routes
Create routes in the routes
directory by importing and using the router
singleton. You can group your routes across several files or keep them
in one file.
router[get|post](pathname, request => ...)
Routes are tied to a pathname and execute their callback when the pathname is encountered.
// in routes/some-file.js
import {router, json} from "primate";
// on matching the exact pathname /, returns {"foo": "bar"} as JSON
router.get("/", () => json`${{"foo": "bar"}}`);
All routes must return a template function handler. See the section on handlers for common handlers.
The callback has one parameter, the request data.
request
object
The The request contains the path
, a /
separated array of the pathname.
import {router, html} from "primate";
router.get("/site/login", request => json`${{"path": request.path}}`);
// accessing /site/login -> {"path":["site","login"]}
The HTTP request's body is available under request.payload
.
Regular expressions in routes
All routes are treated as regular expressions.
import {router, json} from "primate";
router.get("/user/view/([0-9])+", request => json`${{"path": request.path}}`);
// accessing /user/view/1234 -> {"path":["site","login","1234"]}
// accessing /user/view/abcd -> error 404
router.alias(from, to)
To reuse certain parts of a pathname, you can define aliases which will be applied before matching.
import {router, json} from "primate";
router.alias("_id", "([0-9])+");
router.get("/user/view/_id", request => json`${{"path": request.path}}`);
router.get("/user/edit/_id", request => ...);
router.map(pathname, request => ...)
You can reuse functionality across the same path but different HTTP verbs. This
function has the same signature as router[get|post]
.
import {router, html, redirect} from "primate";
router.alias("_id", "([0-9])+");
router.map("/user/edit/_id", request => {
const user = {"name": "Donald"};
// return original request and user
return {...request, user};
});
router.get("/user/edit/_id", request => {
// show user edit form
return html`<user-edit user=${request.user} />`
});
router.post("/user/edit/_id", request => {
const {user} = request;
// verify form and save / show errors
return await user.save() ? redirect`/users` : html`<user-edit user=${user} />`
});
Handlers
Handlers are tagged template functions usually associated with data.
html`<component-name attribute=${value} />`
Compiles and serves a component from the components
directory and with the
specified attributes and their values. Returns an HTTP 200 response with the
text/html
content type.
json`${{data}}`
Serves JSON data
. Returns an HTTP 200 response with the application/json
content type.
redirect`${url}`
Redirects to url
. Returns an HTTP 302 response.
Components
Create HTML components in the components
directory. Use data
-attributes to
show data within your component.
// in routes/user.js
import {router, html, redirect} from "primate";
router.alias("_id", "([0-9])+");
router.map("/user/edit/_id", request => {
const user = {"name": "Donald", "email": "donald@was.here"};
// return original request and user
return {...request, user};
});
router.get("/user/edit/_id", request => {
// show user edit form
return html`<user-edit user=${request.user} />`
});
router.post("/user/edit/_id", request => {
const {user, payload} = request;
// verify form and save / show errors
// this assumes `user` has a method `save` to verify data
return await user.save(payload) ? redirect`/users` : html`<user-edit user=${user} />`
});
<!-- components/edit-user.html -->
<form method="post">
<h1>Edit user</h1>
<p>
<input name="user.name" data-value="user.name"></textarea>
</p>
<p>
<input name="user.email" data-value="user.email"></textarea>
</p>
<input type="submit" value="Save user" />
</form>
data-for
Grouping objects with You can use the special attribute data-for
to group objects.
<!-- components/edit-user.html -->
<form data-for="user" method="post">
<h1>Edit user</h1>
<p>
<input name="name" data-value="name" />
</p>
<p>
<input name="email" data-value="email" />
</p>
<input type="submit" value="Save user" />
</form>
Expanding arrays
data-for
can also be used to expand arrays.
// in routes/user.js
import {router, html} from "primate";
router.get("/users", request => {
const users = [
{"name": "Donald", "email": "donald@was.here"},
{"name": "Ryan", "email": "ryan@was.here"},
];
return html`<user-index users=${users} />`;
});
<!-- in components/user-index.html -->
<div data-for="users">
User <span data-value="name"></span>
Email <span data-value="email"></span>
</div>
Resources
License
BSD-3-Clause