Primate

Responses

Route handlers return a ResponseLike value that Primate converts into a WHATWG Response. You can return simple values ("implicit" responses) or use explicit handlers from primate/response when you need to control all aspects of the response.

Return value Handler Response Notes
string text 200 text/plain Serve plain text
object json 200 application/json Serve JSON
Blob·File·FileRef binary 200 application/octet-stream Stream contents
URL redirect 302 Redirect to URL
view 200 text/html Serve frontend component
error 404 text/html Show error page
ws 101 WebSocket upgrade
sse 200 text/event-stream Server‑sent events
null 204 new Response(null)
Response as given WHATWG Response

Text

Return strings to serve text/plain.

import pema from "pema";
import string from "pema/string";
import route from "primate/route";

route.get(request => {
  const { name } = request.body.fields(pema({ name: string }));

  return `Name '${name}' submitted`;
});

Use the explicit text handler for more options.

import pema from "pema";
import string from "pema/string";
import response from "primate/response";
import Status from "primate/response/Status";
import route from "primate/route";

route.post(request => {
  const { name } = request.body.fields(pema({ name: string.optional() }));

  if (name === undefined) {
    return response.text("No name specified", {
      status: Status.UNPROCESSABLE_ENTITY,
    });
  }

  return response.text(`Name '${name}' submitted`, { status: Status.CREATED });
});

JSON

Return JSON-serializable objects to serve application/json.

import route from "primate/route";

route.get(() => [
  { name: "Donald" },
  { name: "John" },
]);

Use the explicit json handler for more options.

import response from "primate/response";
import Status from "primate/response/Status";
import route from "primate/route";

route.get(() => response.json([
  { name: "Donald" },
  { name: "John" },
], { status: Status.CREATED }));

Binary

Return Blob, File, ReadableStream or any object exposing { stream(): ReadableStream } to serve application/octet-stream (binary data).

import route from "primate/route";

route.get(() => new Blob(["data"]));

Primate attempts to read the source's name and MIME type if available. Use the explicit binary handler for more options.

import binary from "primate/response/binary";
import route from "primate/route";

route.get(() => binary(new Blob(["data"]), {
  // set filename manually
  headers: { "Content-Disposition": "attachment; filename=data.bin" },
}));

Use rcompat's FileRef to conveniently load a file from disk and stream it out.

import FileRef from "@rcompat/fs/FileRef";
import route from "primate/route";

route.get(() => new FileRef("/tmp/data.bin"));

Redirect

Return a URL to redirect to another address.

import route from "primate/route";

route.get(() => new URL("https://example.com/login"));

Use the explicit redirect handler to vary the status or for local redirects.

import Status from "@rcompat/http/Status";
import response from "primate/response";
import route from "primate/route";

// another status
route.get(() => response.redirect("https://primate.run", Status.SEE_OTHER));

// local redirect
route.post(request => response.redirect(`/login?next=${request.target}`));

View

Render and serve components from the components directory as text/html.

import response from "primate/response";
import route from "primate/route";

// render components/Counter.jsx, embed in pages/app.html and serve
route.get(() => response.view("Counter.jsx"));

Props

Populate the component with initial props.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("Counter.jsx", { start: 10 }));

Page

Components are embedded into your app's main HTML page at pages/app.html, with the component code replacing the %body% placeholder. If the app page doesn't exist, Primate falls back to its standard one.

<!doctype html>
<html>
  <head>
    <title>Primate app</title>
    <meta charset="utf-8" />
    %head%
  </head>
  <body>%body%</body>
</html>

Pass a different page option to use another HTML page.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("Counter.jsx", { start: 10 },
  { page: "counter.html" })); // render into `pages/counter.html`

Placeholders

You can use placeholders in your HTML pages.

<!doctype html>
<html>

<head>
  <title>%title%</title>
  <meta charset="utf-8" />
  %head%
</head>

<body>%body%</body>

</html>

Populate them in your routes.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.view("Counter.jsx", { start: 10 }, {
  placeholders: {
    title: "Counter",
  },
}));

Partial

Pass a partial: true option to render the component without the enclosing HTML page.

import response from "primate/response";
import route from "primate/route";

// will render Counter without embedding it into pages/app.html
route.get(() => response.view("Counter.jsx", { start: 10 }, { partial: true }));

This is useful for replacing parts of the page whilst retaining the HTML page.

Error

Serve a 404 Not Found error page as text/html.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.error({
  body: "Not Found", // default
}));

This handler uses the HTML file at pages/error.html or falls back to a standard one provided by Primate.

<!doctype html>
<html>
  <head>
    <title>Error page</title>
    <meta charset="utf-8" />
    %head%
  </head>
  <body>
    <h1>Error page</h1>
    <p>
      %body%
    </p>
  </body>
</html>

You can pass a custom status to this handler.

import Status from "@rcompat/http/Status";
import response from "primate/response";
import route from "primate/route";

route.get(() => response.error({
  status: Status.INTERNAL_SERVER_ERROR,
}));

As with view, you can pass a different page option to use another HTML page.

import response from "primate/response";
import route from "primate/route";

// use pages/custom-error.html instead of pages/error.html
route.get(() => response.error({ page: "custom-error.html" }));

WebSocket

Upgrade a GET request to ws: and handle open, message, and close events.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.ws({
  open(socket) {
    socket.send("hello");
  },
  message(socket, message) {
    // echo
    socket.send(String(message));
  },
  close(socket) {
    console.log("socket closed");
  },
}));

Server‑sent events

Push out events to the client as text/event-stream.

import response from "primate/response";
import route from "primate/route";

route.get(() => response.sse({
  // connection opened
  open(source) {
    // push event to client
    source.send("open", "hi!");
  },
  // connection closed
  close() { },
}));

Response

Return a custom Response.

import Status from "primate/response/Status";
import route from "primate/route";

route.get(() => new Response("Hi!", {
  status: Status.ACCEPTED,
  headers: { "X-Custom": "1" },
}));

ResponseLike reference

type ResponseLike =
  | string
  | Record<string, unknown>
  | Record<string, unknown>[]
  | Blob
  | ReadableStream
  | URL
  | null
  | Response
  ;
Previous
Requests
Next
Validation