Write a surface

REST, GraphQL, the call envelope and the terminal are four doors onto the same operations. A surface of your own — a webhook, a feed — is one more door, written the same way.

A door

A door says how a request becomes calls, and how their outcome becomes an answer — in its own terms. Core runs every door through one cycle, app.answer(door, received):

import { createAppRunner, type Door } from '@fougere/core';

/** A payment provider's webhook: one POST, one call. */
const webhook: Door<Request, { id: string; status: string }, Response> = {
  name: 'webhook',
  async decode(request) {
    const event = await request.json();
    return { id: event.data.id, status: event.type };
  },
  run: ({ id, status }) => call({ address: 'order', op: 'settle' },
    { params: { id }, query: {}, input: { status }, state: {} }),
  encode(outcome) {
    return 'refused' in outcome
      ? Response.json({ error: outcome.refused.code }, { status: 400 })
      : new Response(null, { status: 204 });
  },
};

const call = createAppRunner(app);
hono.post('/hooks/psp', (c) => app.answer(webhook, c.req.raw));
decodeturns what arrived into what the calls need — read within maxBodyBytes
runmakes the calls: one, or several for a request that asks for several things
encodesays the outcome — { answered } or { refused } — in the door's shape

What the cycle adds, the same for every door:

  • the request is entered, so every call it makes says which request it answers;
  • received, decoded, encoded and failed are announced to whoever observes the app, beside the calls;
  • the caller's state is read and written, when the door says what carries it (carries);
  • a refusal is made public by one rule before the door says it — an INTERNAL_ERROR never shows its message, unless the door is trusted (the terminal, whose caller is the operator).

Core names no door: the cycle is the same for one it has never heard of. A simpler bridge — parse, call, format, outside any cycle — is invoke.

Built with Fougere and Nuxt, hosted on GitHub Pages.