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 => {
  // Serve strings as plain text
  router.get("/user", () => "Donald");
};

JSON

import {File} from "runtime-compat/filesystem";

export default router => {
  // Serve proper JavaScript objects as JSON
  router.get("/users", () => [
    {name: "Donald"},
    {name: "Ryan"},
  ]);

  // Load from 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 Response object for custom response status
  router.get("/create", () => new Response("created!", {status: 201}));
};

HTML

// Use an explicit handler as we can't detect HTML by the return value type
export default (router, {html}) => {
  // Embed components/hello-world.html into static/index.html and serve it. In
  // case a user-provided index.html doesn't exist, use a fallback index.html
  router.get("/hello", () => html("hello-world"));

  // Same as above, but without embedding
  router.get("/hello-partial", () => html("hello-world", {partial: true}));

  // Serve directly from string instead of loading a component
  router.get("/hello-adhoc", () => html("<p>Hello, world!</p>", {adhoc: true}));
};

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

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 on this route
  router.map("edit-user", ({body}) => body?.name ?? "Donald");

  // Show user as plain text
  router.get("edit-user", user => user);

  // Verify or show error
  router.post("edit-user", user => user === "Donald"
    ? "Hi Donald!"
    : {message: "Error saving user"});
};

Explicit handlers

Most often we can figure out the content type to respond with based on the return type from the handler. To handle content not automatically detected, use the second argument of the exported function.

export default (router, {redirect}) => {
  // redirect from source to target
  router.get("/source", () => redirect("/target"));
};

Resources

License

MIT