Skip to content

Front-end app: connect form contracts to mock API with generated clients #9

Description

@mryhmln

Note: Moved from #36. Not yet committed to doing this — preserved for future consideration.

Summary

Create a front-end application that connects the form contracts (designed in Storybook) to the mock API server via the generated TypeScript clients. The same .form.yaml files authored and previewed in Storybook become the runtime form configuration, with data fetched from the API instead of static YAML fixtures.

Motivation

All the pieces exist but aren't wired together:

Layer Package Status
OpenAPI specs @safety-net/contracts 5 specs (applications, persons, households, incomes, users)
Mock server @safety-net/mock-server Express + SQLite on port 1080, CRUD routes auto-generated from specs
TypeScript API clients @safety-net/clients Generated from specs with Zod validation
Form contracts + renderer @safety-net/harness YAML-driven forms with composable layouts

The harness loads test data from static YAML imports. The mock server exposes REST endpoints. The generated clients provide typed fetch functions. The front end is the glue that connects them.

Design

Contract-driven workflow

Author form contract in Storybook (with static fixtures)
    ↓
Same .form.yaml drives the front-end app
    ↓
Data fetched from API via generated TypeScript clients
    ↓
FormRenderer renders the contract with live data
    ↓
Submissions write back through the API

The form contracts are the single source of truth for UI layout. Storybook is the design environment; the front end is the runtime environment. Same contracts, different data sources.

App structure

A React app shell with routing that maps URLs to contract-driven views:

/applications              → list of applications (data-table)
/applications/:id          → FormRenderer with contract + API data
/applications/new          → FormRenderer with contract (empty, create mode)
/households                → list of households
/households/:id            → FormRenderer with contract + API data

Data flow

// List view — fetch from API, render as table
const { data } = await ApplicationsService.listApplications();
// Table component renders rows, click navigates to detail

// Detail view — fetch by ID, render with FormRenderer
const application = await ApplicationsService.getApplication({ id });
<FormRenderer
  contract={intakeContract}        // same .form.yaml from Storybook
  schema={ApplicationZodSchema}    // same Zod schema
  defaultValues={application}      // data from API, not YAML fixture
  onSubmit={(values) => ApplicationsService.updateApplication({ id, body: values })}
/>

Scope

Phase 1: Minimal app shell

  • React app with routing (new mock-app workspace package)
  • Route manifest — explicit routes.yaml maps URL paths to contract IDs
  • Contract loader — imports .form.yaml contracts at build time
  • API integration — wire generated clients from @safety-net/clients to FormRenderer
  • Detail view — route param → API fetch → FormRenderer with live data
  • Submit handler — FormRenderer onSubmit → API update call
  • Role switching — select role to see different permission levels (like Storybook)

Phase 2: List views + navigation

Phase 3: Full workflow

  • Multi-schema workspace views (households + applications + incomes in one UI)
  • Search and filtering
  • Pagination
  • Optimistic updates / loading states
  • Error handling for API failures

Dependencies


Architecture Decisions

The following decisions were made after discussing the open questions in the original issue.


1. New mock-app package (not in harness)

Decision: Create a new packages/mock-app workspace package. Rename packages/harness to packages/form-designer.

Why: Harness has a clear purpose — it's the Storybook-based design environment for authoring and previewing contracts with static fixtures. Adding a runtime app with routing, API calls, and state management muddies that focus. The two packages also have different dependencies (the app needs React Router, API client integration; the designer doesn't) and different build targets (Storybook vs Vite dev server).

Alternatives considered:

  • Extend harness — Would bloat harness with routing/API deps, couple design-time and run-time concerns, and create confusion about what harness is for. Rejected.

Resulting package structure:

Package Purpose
contracts OpenAPI specs
clients Generated API clients
mock-server Fake backend (port 1080)
form-designer Storybook — author/preview contracts (renamed from harness)
mock-app React app — contracts + live API

Both form-designer and mock-app consume the same .form.yaml contracts and Zod schemas. The contracts are the shared interface — authored in the designer, executed in the app.

mock-server stays as its own package because the mock app consumes its API but they run as separate processes with different concerns. The server is also used independently for integration testing in clients.


2. Explicit app-level route manifest (no convention-based routing)

Decision: An explicit routes.yaml manifest in the mock-app package maps URL paths to contract IDs. Contracts stay route-free — no route field in .form.yaml.

Why: Routes are a developer/app concern, not something a form designer should think about. Keeping contracts route-free means they stay purely about what to render, while the manifest handles where to render it. Multiple apps could use the same contracts with different route structures (e.g., an applicant-facing app and a caseworker portal both referencing application-intake at different paths). The manifest is also easy to scan — a developer can see the app's full surface area in one file.

Example:

# packages/mock-app/routes.yaml
routes:
  - path: /applications
    contract: case-list
  - path: /applications/new
    contract: application-intake
  - path: /applications/:id
    contract: caseworker-review

Alternatives considered:

  • Convention-based (derive routes from schema/ID) — Sounds appealing but fragile. Deriving /applications from schema: Application requires pluralization and case conversion that breaks easily (what route does schema: ExpeditedSNAPInfo produce?). Also makes the app's routing implicit — you can't see what routes exist without understanding the convention rules. Rejected.
  • Route field in the contract YAML — Mixes app-level concerns into designer-authored files. Not designer-friendly, and couples contracts to a specific app's URL structure. This is atypical — CMS-style systems (Strapi, Contentful, React Admin) all keep routing in app code, not in content/schema definitions. Rejected.

3. Mock-only for now, but don't hardcode it

Decision: The app targets the mock server only. No abstraction layers for hypothetical future backends.

Why: There's no real backend to connect to yet. Building abstraction layers for one would be premature — we'd be guessing at the interface. However, the architecture naturally avoids tight coupling: the app imports from @safety-net/clients (typed service calls like ApplicationsService.getApplication()), not raw fetch('http://localhost:1080/...'). The base URL comes from environment config (VITE_API_URL). If a real backend appears later, changing the env var is all that's needed — the app code doesn't change.

The name mock-app sets the right expectation: this is a reference implementation and living specification for how contracts connect to APIs, not a production app.

Alternatives considered:

  • Build backend abstraction layer now — Would require guessing at the interface of a backend that doesn't exist yet. Premature abstraction. The generated clients already provide the right level of indirection. Rejected.
  • Support real + mock from the start — No real backend exists. The generated clients + env-based URL config already make switching trivial when the time comes. No extra work needed now. Rejected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions