@kequtech/arbor

Getting Started

Arbor works directly with Node's HTTP server. You define routes and actions, and Arbor executes them in a clear sequence. This page shows the minimal setup required to get an application running.

Installation

npm i @kequtech/arbor

Project Layout

A simple Arbor project uses two files. One file starts the HTTP server. The other defines the Arbor application. This keeps the application testable without involving network code.

project/
- server.ts
- app.ts

A Minimal Application

// server.ts
import { createServer } from "node:http";
import app from "./app.ts";

createServer(app).listen(4000, () => {
  console.log("Listening on http://localhost:4000");
});
// app.ts
import { createApp } from "@kequtech/arbor";

export default createApp({
  routes: [
    {
      method: "GET",
      url: "/",
      actions: [() => "Hello world"],
    },
  ],
});

Run the server:

node server.ts

Visit:

http://localhost:4000/

Arbor resolves both GET and HEAD requests through this route by default.

Adding Behavior With Actions

Actions define the logic for a request. They can modify headers, read the body, produce a payload or finalize the response.

// app.ts
import { createApp, createAction } from "@kequtech/arbor";

const actionSetJson = createAction(({ res }) => {
  res.setHeader("Content-Type", "application/json");
});

export default createApp({
  routes: [
    {
      method: "GET",
      url: "/status",
      actions: [
        actionSetJson,
        () => ({ ok: true }),
      ],
    },
  ],
});

Arbor selects an appropriate renderer based on the Content-Type set by the res header.

Async actions

Actions may be asynchronous. Arbor waits for each one before running the next.

// example: loading data asynchronously
const actionLoadUser = createAction(async ({ params, context }) => {
  context.user = await db.findUser(params.id);
});

const routeUser = createRoute({
  method: "GET",
  url: "/users/:id",
  actions: [
    actionSetJson,
    actionLoadUser,
    ({ context }) => ({ user: context.user }),
  ],
});

Async actions behave exactly like synchronous ones: return a value to finish, throw to signal an error, or let the chain continue.

Grouping Routes With Branches

Branches let you organize related routes and share actions. They attach behavior to a URL prefix and apply it consistently.

// app.ts
import { createApp, createBranch } from "@kequtech/arbor";

const branchApi = createBranch({
  url: "/api",
  actions: [actionSetJson],
  routes: [
    {
      method: "GET",
      url: "/users",
      actions: [() => ({ users: [] })],
    },
  ],
});

export default createApp({
  branches: [branchApi]
});

Reading the Request Body

Arbor only parses the body when you ask for it. Use getBody inside an action.

// app.ts
import { createAction, createApp } from "@kequtech/arbor";

const actionUsers = createAction(async ({ getBody }) => {
  const body = await getBody({ trim: true });
  return `Received ${body.name}`;
});

export default createApp({
  routes: [
    {
      method: "POST",
      url: "/users",
      actions: [actionUsers],
    },
  ],
});

getBody supports multipart uploads, raw buffers, normalization, validation maps and optional non-throwing behavior. These are explained in detail in the Body section.

Where to Go Next

Move to Core to learn Arbor’s building blocks in depth:

  • Branches
  • Routes
  • Actions
  • Request Bundle
  • Body
  • Renderers
  • Errors
  • Static Files

Once you understand those pieces, the Guides section shows recommended patterns for real applications.