Skip to main content

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 of1:1, 1:n and n: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.

The request object

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>

Grouping objects with data-for

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