Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

ResourceMachine is an HTTP decision machine for building API gateways and microservices in Node.js.

It is a port of Webmachine, the Erlang library that made HTTP correctness a first-class concern. The idea: instead of writing request handlers that manually branch on method, headers, and state, you describe your resource as a set of simple boolean and map methods, and a decision machine walks the HTTP diagram for you.

The problem it solves

Most HTTP frameworks give you a blank canvas. You write a handler, you check the method, you set a status code, and you ship it. That works fine — until you need to handle conditional requests, serve multiple content types from one endpoint, or implement correct cache validation. At that point you’re implementing a non-trivial chunk of RFC 7230–7235 by hand, and the edge cases accumulate quietly.

ResourceMachine handles the protocol layer. You handle the business logic.

The model

Each route maps to a Resource class. The decision machine constructs a new instance per request and walks ~40 decision points, calling your methods when it needs answers:

  • Is the service available?
  • Is this method allowed?
  • Is the request authorized? Forbidden?
  • Does the resource exist?
  • Has it been modified since the client last fetched it?
  • Which content types can you provide?
  • Which content types can you accept on PUT / PATCH?

Override only what’s relevant. Everything else defaults to safe, correct HTTP behavior.

import { createServer, Resource } from "resource-machine";

class PingResource extends Resource {
	override async contentTypesProvided() {
		return {
			"application/json": () => JSON.stringify({ ok: true }),
		};
	}
}

const server = createServer({ name: "my-api" });
server.addRoute("/ping", PingResource);
await server.listen(3000);

A GET /ping with Accept: application/json returns 200 with the JSON body. A request with Accept: text/html returns 406. A DELETE /ping returns 405. None of that required a single if statement.

Per-request instantiation

The router holds your class. The machine calls new YourResource(req, res) for each request. This means:

  • this.req and this.res are available in every method — no threading parameters through callbacks.
  • this is a natural place to cache DB lookups across decision calls. Fetch in resourceExists(), reuse in contentTypesProvided() — one round trip per request, not one per method call.
  • Per-request instances mean zero shared mutable state between concurrent requests.

Shared resources (database pools, config, caches) belong on class statics or module scope, not on this.

export function createUserResource(db: Database): ResourceClass {
	return class UserResource extends Resource {
		private user: User | null = null;

		override async resourceExists() {
			this.user = await db.users.find(this.req.params.id);
			return this.user !== null;
		}

		override async isAuthorized() {
			if (this.req.authorization?.token) return true;
			return 'Bearer realm="api"';
		}

		override async contentTypesProvided() {
			return {
				"application/json": () => JSON.stringify(this.user),
			};
		}
	};
}

What you get for free

By filling in your resource methods, you automatically get correct behavior for:

ConcernHow
Method enforcementallowedMethods()405 Method Not Allowed
Content negotiationcontentTypesProvided() + Accept406 or matched type
AuthorizationisAuthorized() returning string → 401 + WWW-Authenticate
Conditional GETgenerateETag() + If-None-Match304 Not Modified
PreconditionsIf-Match / If-Unmodified-Since412 Precondition Failed
Missing resourcesresourceExists()404 Not Found
Body validationcontentTypesAccepted()415 Unsupported Media Type
Payload limitsmaxBodySize option → 413 Payload Too Large
HEAD requestsHandled automatically — no extra code
OPTIONSReturns Allow header, no extra code
Vary headerComputed automatically from negotiation results

What it is not

ResourceMachine is not a general-purpose web framework. It has no template engine, no session middleware, no static file serving built in (though you can implement a static resource yourself). It is optimized for one thing: making it easy to write HTTP APIs that behave correctly.

Next steps