Files
suaveplan/packages/api/api-plugin-banking/src/routes/build-routes.md
2026-05-27 23:29:53 +01:00

4.9 KiB

Banking routes

Purpose

This module builds the OpenAPIHono sub-app exposed by the banking plugin. It takes a BankingRoutesDeps containing the BankingService and an optional typed emitEvent callback, and returns a Hono app that mounts /banks and /accounts route trees. Every route consumes the Zod schemas from schemas/schemas.ts so the generated OpenAPI document is correct, every error path maps a BankingError to a precise HTTP status, and every state-changing route fires a typed event through emitEvent when supplied.

Features

  • 14 HTTP routes total: 7 on /banks (list, create, by-id, by-sort-code, search, update, soft-delete) and 7 on /accounts (list, create, by-id, by-number, by-user, get-owner, update, delete, status, transfer, add-funds, subtract-funds — list above combines the count).
  • Uniform error mapping: a single errorBody(err) helper maps every thrown BankingError to the canonical status; non-BankingError throws become 500.
  • Typed events emitted on every state transition; the emitEvent callback's signature is constrained by the BankingEventMap union so there are no casts at emit sites.
  • Discriminated RepositoryResponse outcomes from transferFunds and subtractFunds are flattened to (status, body) pairs without exception-driven control flow.

Usage: Basic

import { createBankingRoutes, BankingService } from "@suaveplan/api-plugin-banking";

const service = new BankingService(adapter, config);
const app = createBankingRoutes({ service });
host.route("/banking", app);

Usage: Advanced

Wire the event emitter to a typed event bus so audit, projection, and notification plugins observe every transition:

import { createBankingRoutes } from "@suaveplan/api-plugin-banking";
import type { BankingEventMap } from "@suaveplan/api-plugin-banking";
import { EventBus } from "@suaveplan/events";

const bus = new EventBus<BankingEventMap>();
bus.on("banking:transfer:succeeded", (e) => audit.log(e));

const app = createBankingRoutes({
    service,
    emitEvent: (type, payload) => bus.emit(type, payload),
});

The OpenAPI spec is generated by @hono/zod-openapi from each route's request / responses Zod schemas — call OpenAPIHono.doc("/openapi.json", { ... }) on the parent app to expose it.

API Reference

interface BankingRoutesDeps

  • service: BankingService — the coordinator; routes resolve every repository call through it so policy and event emission stay in one place.
  • emitEvent?: <K extends keyof BankingEventMap>(type: K, payload) => void — optional typed event emitter; if absent, every state transition silently proceeds without fan-out.

createBankingRoutes(deps): OpenAPIHono

Returns the sub-app. Mount it at any prefix; the route paths themselves are relative.

Route catalogue

GET /banks — list (filterable by status, site_id, paginated). POST /banks — create. Emits banking:bank:created. GET /banks/{id} — by id (404 on miss). PATCH /banks/{id} — update. Emits banking:bank:updated. DELETE /banks/{id} — soft-delete. Emits banking:bank:soft-deleted. GET /banks/sort-code/{sortCode} — by XX-XX-XX sort code. GET /banks/search/{query} — case-insensitive name search.

GET /accounts — list (filterable by user_id, bank_id, status). POST /accounts — create. Emits banking:account:created. GET /accounts/{id} — by id. PUT /accounts/{id} — update. Emits banking:account:updated. DELETE /accounts/{id} — delete. Emits banking:account:deleted. PATCH /accounts/{id}/status — status FSM. Emits banking:account:status-changed. POST /accounts/transfer — fund transfer. Emits banking:transfer:succeeded or banking:transfer:failed. POST /accounts/{id}/add-funds — add funds. POST /accounts/{id}/subtract-funds — subtract funds. GET /accounts/by-number/{accountNumber} — by numeric account-number. GET /accounts/user/{userId} — list a user's accounts. GET /accounts/{id}/owner — owner user-id lookup.

Implementation Notes

Routes that may produce both 200/204 success and an error status code return c.json(body, statusNumber) rather than relying on the Hono default — this lets the errorBody helper map every BankingError.code to a precise status without re-running the narrowing logic in each route handler.

emitEvent is invoked after the underlying repository write succeeds. Failure-mode events (banking:transfer:failed) are emitted on the negative branch so projections can record the attempt even when no balance changed. This matches the upstream package's contract that "every transfer attempt is observable".

The route layer does NOT re-parse request bodies with safeParse — @hono/zod-openapi already validates via c.req.valid("json") before the handler runs, and any validation failure short-circuits with a 400 before the handler executes. Catching at the handler level only sees thrown BankingErrors and unexpected database failures.