|
| 1 | +# FR-008 — Universal React/UI hookup: per-port REST route codegen + Angular 18 client + API contract conformance |
| 2 | + |
| 3 | +- **Date:** 2026-05-26 |
| 4 | +- **Status:** Design — plan-of-record. Captures the cross-port work needed so any backend (TS / Java / Kotlin / C# / Python) can serve a universal browser-side UI client (React/TanStack today, Angular 18 added by this FR). |
| 5 | +- **Target version:** 7.0.0 + follow-ups |
| 6 | +- **Scope:** Five workstreams that together close the React/UI hookup gap across all language ports: |
| 7 | + 1. **Java route codegen** — Spring `@RestController` per entity matching the cross-port URL grammar |
| 8 | + 2. **Kotlin route codegen** — Spring-Kotlin or Ktor controller per entity (substrate decision needed) |
| 9 | + 3. **Python route codegen** — FastAPI `APIRouter` per entity |
| 10 | + 4. **C# route codegen refinement** — gap audit + Angular 18 client integration for ASP.NET Minimal API backends (already shipped) |
| 11 | + 5. **Angular 18 client tier** — `client/web/packages/angular/` runtime + `server/typescript/packages/codegen-ts-angular/` codegen (universal browser client; consumes any backend via the API contract) |
| 12 | + 6. **API contract conformance corpus** — cross-port runner that verifies every backend's emitted routes conform to the URL grammar + wire format |
| 13 | + |
| 14 | +## 1. Background |
| 15 | + |
| 16 | +The TS browser client (`client/web/packages/{runtime-web,react,tanstack}`) is **architecturally universal** — any backend implementing the URL grammar + wire format from [`docs/features/api-contract.md`](../../docs/features/api-contract.md) can serve it. The contract: |
| 17 | + |
| 18 | +- `GET /api/<entity>?filter[field][op]=value&sort=field:asc&limit=N&offset=N` |
| 19 | +- `GET /api/<entity>/:id` |
| 20 | +- `POST /api/<entity>` / `PATCH /api/<entity>/:id` / `PUT /api/<entity>/:id` / `DELETE /api/<entity>/:id` |
| 21 | +- 8 filter operators (eq / ne / gt / gte / lt / lte / in / like / isNull) gated by field subtype |
| 22 | +- JSON wire format, currency as integer minor units, ISO 8601 dates, `?withCount=1` pagination envelope |
| 23 | + |
| 24 | +**Today's state:** |
| 25 | + |
| 26 | +| Port | Route codegen | Status | |
| 27 | +|---|---|---| |
| 28 | +| TypeScript | `routesFile()` → Fastify via `runtime-ts/drizzle-fastify` | ✓ shipped | |
| 29 | +| C# | `RoutesGenerator.cs` → ASP.NET Minimal API | ✓ shipped (needs Angular-side polish) | |
| 30 | +| Java | — | ✗ consumers hand-write Spring controllers | |
| 31 | +| Kotlin | — | ✗ consumers hand-write Spring-Kotlin or Ktor handlers | |
| 32 | +| Python | — | ✗ consumers hand-write FastAPI routers | |
| 33 | + |
| 34 | +For the React client, the TS + C# backends light up end-to-end; the other three require consumers to hand-write controllers that conform. |
| 35 | + |
| 36 | +For the Angular client, **none** of the backends ship a codegen story today — adding it is part of this FR. The Angular runtime + codegen package live under TypeScript (`client/web/packages/angular/` + `server/typescript/packages/codegen-ts-angular/`) because Angular IS TypeScript; the codegen output is consumed by any backend the same way as React. |
| 37 | + |
| 38 | +## 2. Decomposition into 5 sub-projects |
| 39 | + |
| 40 | +Each sub-project gets its own implementation plan; this FR is the umbrella that locks the cross-port contract + tracks the matrix. |
| 41 | + |
| 42 | +### 2.1 Java route codegen (Spring Boot 3 / Java 21) |
| 43 | + |
| 44 | +New module: `server/java/codegen-spring/` (mirrors `codegen-kotlin/`). |
| 45 | + |
| 46 | +- Extends `codegen-base`'s `MultiFileDirectGeneratorBase<MetaObject>` |
| 47 | +- One generator per concern (mirrors TS pattern): |
| 48 | + - `SpringControllerGenerator` — emits `<Entity>Controller.java` with `@RestController` + 5 CRUD endpoints |
| 49 | + - `SpringDtoGenerator` — emits `<Entity>Dto.java` records (Java 21 records) for request/response — separates wire from persistence entity |
| 50 | + - `SpringFilterAllowlistGenerator` — emits server-side filter allowlist per entity (mirrors TS Project D) |
| 51 | +- Filter operators wired via Spring Data `Specification` OR raw Criteria API |
| 52 | +- Pagination + sort via `Pageable` from Spring Data; output normalized to the wire `{ rows, total }` envelope |
| 53 | +- Currency: minor-units `long` throughout; no conversion at the controller layer |
| 54 | +- Targets: Spring Boot 3.x (Spring Web MVC; not Spring WebFlux — consumers can wire WebFlux separately if needed) |
| 55 | +- New deps to scope: `metaobjects-codegen-base` + `metaobjects-omdb` (or `metaobjects-metadata` only, and route codegen is substrate-agnostic; consumer wires their own persistence — TBD design choice). |
| 56 | + |
| 57 | +### 2.2 Kotlin route codegen — substrate decision |
| 58 | + |
| 59 | +**Option A: Spring-Kotlin controllers.** New generator in `codegen-kotlin/` (already a Kotlin module). Emits `<Entity>Controller.kt` with `@RestController` + Kotlin extension functions. Plays nicely with existing `KotlinSpringConfigGenerator`. |
| 60 | + |
| 61 | +**Option B: Ktor route handlers.** New generator emitting `<Entity>Routes.kt` with `Route.<Entity>Routes()` extension installing the 5 CRUD endpoints on a Ktor `Application`. More Kotlin-native; not Spring-coupled. |
| 62 | + |
| 63 | +**Recommendation:** ship Spring-Kotlin first (party-lore-class consumers are Spring-based). Add Ktor as a follow-up substrate once a Ktor-stack consumer surfaces. |
| 64 | + |
| 65 | +### 2.3 Python route codegen (FastAPI) |
| 66 | + |
| 67 | +Extends Python's codegen module (lives at `server/python/src/metaobjects/codegen/` or similar — verify existing layout when implementation begins). |
| 68 | + |
| 69 | +- Emits `<entity>_router.py` with FastAPI `APIRouter` + 5 CRUD endpoints |
| 70 | +- Pydantic v2 request/response models (consumers already use Pydantic per the existing `field_types` codegen output) |
| 71 | +- Pagination + sort via Query params; filter via the same URL grammar |
| 72 | +- Currency: minor-units `int`; no float conversion |
| 73 | +- Targets: FastAPI 0.110+; consumer wires `APIRouter` instances into their FastAPI `app` |
| 74 | + |
| 75 | +### 2.4 C# route refinement + Angular 18 hookup |
| 76 | + |
| 77 | +`MetaObjects.Codegen/Generators/RoutesGenerator.cs` already ships ASP.NET Minimal API routes for .NET 8 / C# 12. The refinement work: |
| 78 | + |
| 79 | +- **Gap audit** vs the cross-port URL grammar — verify filter operators, pagination envelope, sort syntax all conform |
| 80 | +- **withCount=1 envelope** support — ensure the routes return `{ rows, total }` when requested |
| 81 | +- **PATCH vs PUT** — match the TS contract (TS supports both verbs for updates; C# should too) |
| 82 | +- **CORS preflight** helpers in the consumer setup recipe (so an Angular dev-server on port 4200 can call an ASP.NET backend on port 5000) |
| 83 | +- **Angular-side integration** — generated Angular services target ASP.NET Minimal API URL conventions cleanly (no special-casing needed; this is the validation that the cross-port grammar holds) |
| 84 | + |
| 85 | +### 2.5 Angular 18 client tier (NEW universal client) |
| 86 | + |
| 87 | +**Two new packages on the TypeScript side:** |
| 88 | + |
| 89 | +#### `client/web/packages/angular/` — Angular 18 runtime |
| 90 | + |
| 91 | +Mirrors `client/web/packages/react/` shape but for Angular. Universal — works with any backend that conforms to the API contract. |
| 92 | + |
| 93 | +- **`EntityFetcherToken`** — Angular `InjectionToken` for the `EntityFetcher` function (consumer provides via `provideEntityFetcher(fetcher)`) |
| 94 | +- **`CurrencyInputComponent`** (`<mo-currency-input>`) — standalone Angular component mirroring React's `<CurrencyInput>` for minor-units bidirectional binding |
| 95 | +- **`EntityGridComponent`** (`<mo-entity-grid>`) — standalone component over TanStack Table Angular adapter (`@tanstack/angular-table` 8.x) |
| 96 | +- **`CellRendererRegistry`** — Angular DI-friendly registry for cell renderer overrides per view subtype |
| 97 | +- **`buildFilterQs`** — re-export from `runtime-web` (framework-agnostic; no Angular-specific re-implementation needed) |
| 98 | +- **`formatCurrency` / `parseCurrency`** — re-export from `runtime-web` |
| 99 | +- Angular 18 specifically: standalone components only (no NgModules), signals-based reactivity where idiomatic |
| 100 | + |
| 101 | +#### `server/typescript/packages/codegen-ts-angular/` — Angular codegen |
| 102 | + |
| 103 | +Mirrors `codegen-ts-react/` + `codegen-ts-tanstack/` pair. One package; emits both forms + service code. |
| 104 | + |
| 105 | +- **`angularServiceFile()`** — emits `<Entity>.service.ts` per entity: `@Injectable({ providedIn: 'root' })` class wrapping the EntityFetcher with typed methods (`list`, `get(id)`, `create(dto)`, `update(id, patch)`, `delete(id)`) |
| 106 | +- **`angularFormFile()`** — emits `<Entity>.form.component.ts` per entity: standalone form component using Angular reactive forms + the metadata-driven validators (mirrors React's `useEntityForm`) |
| 107 | +- **`angularGridFile()`** — emits `<Entity>.grid.component.ts` per entity with `layout.dataGrid` metadata: standalone grid component pre-wired with column defs derived from metadata + TanStack Angular Table integration |
| 108 | +- **Per-entity opt-out**: `@emitAngular: false` on an entity skips all three Angular outputs |
| 109 | + |
| 110 | +#### Why this lives in the TS workspace, not C# |
| 111 | + |
| 112 | +Angular IS TypeScript — the generated output is `.ts` files consumed by an Angular CLI project. The codegen package is TS that emits TS. The natural home is `server/typescript/packages/codegen-ts-angular/` (parallel to `codegen-ts-react` + `codegen-ts-tanstack`), and the runtime package is `client/web/packages/angular/` (parallel to `client/web/packages/react`). |
| 113 | + |
| 114 | +**C# .NET 8 / C# 12 backends consume the Angular client the same way they'd consume the React client** — via the API contract. The "C# + Angular 18" combination becomes a documented recipe in `docs/recipes/csharp-angular18.md` (new) explaining the CORS + base-URL wiring, not a new codegen module under `server/csharp/`. |
| 115 | + |
| 116 | +### 2.6 API contract conformance corpus |
| 117 | + |
| 118 | +New shared corpus: `fixtures/api-contract-conformance/` (NEW). Goal: any backend's emitted routes should pass this corpus' assertions identically. |
| 119 | + |
| 120 | +Fixtures shape: |
| 121 | + |
| 122 | +``` |
| 123 | +fixtures/api-contract-conformance/<scenario-name>/ |
| 124 | +├── meta.json # canonical metadata for the entity under test |
| 125 | +├── seed.json # seed rows |
| 126 | +├── requests.yaml # ordered HTTP requests (method + path + body) |
| 127 | +└── expected.json # per-request expected status + body shape |
| 128 | +``` |
| 129 | + |
| 130 | +Per-port runner (one per shipped backend codegen, including TS + C# already-shipped + Java/Kotlin/Python/C#-Angular-route after FR-008.x lands): |
| 131 | + |
| 132 | +1. Generate routes for the fixture's metadata |
| 133 | +2. Run the generated backend (TS: Fastify; Java: Spring Boot test slice; Kotlin: same; C#: WebApplicationFactory; Python: FastAPI TestClient) |
| 134 | +3. Execute `requests.yaml` against the running backend via HTTP |
| 135 | +4. Assert each response matches `expected.json` (status + body, normalized for non-deterministic fields like timestamps) |
| 136 | + |
| 137 | +Scenarios to ship Day 1 (12+): |
| 138 | + |
| 139 | +- `list-empty` — GET /api/Author → `[]` |
| 140 | +- `list-with-pagination` — GET /api/Author?limit=10&offset=20 → page slice |
| 141 | +- `list-with-withcount` — GET /api/Author?limit=10&withCount=1 → `{ rows, total }` |
| 142 | +- `filter-eq` / `filter-ne` / `filter-gt` / `filter-like` / `filter-in` / `filter-isnull` — one per operator |
| 143 | +- `sort-asc` / `sort-desc` |
| 144 | +- `get-by-id` / `get-by-id-not-found` |
| 145 | +- `create-201` / `create-400-validation-error` |
| 146 | +- `update-patch` / `update-put` |
| 147 | +- `delete-204` / `delete-not-found` |
| 148 | + |
| 149 | +This is the analog of `persistence-conformance/` for the API tier. Reuses the same canonical entity (`Author` / `acme::blog`) so the metadata stays shared. |
| 150 | + |
| 151 | +## 3. Cross-port classification per `cross-language-porting` |
| 152 | + |
| 153 | +### Tier 1 — invariant (must match cross-port) |
| 154 | + |
| 155 | +- URL grammar (paths + verbs + filter syntax + sort syntax + pagination params) |
| 156 | +- Wire format (JSON, currency as integer minor-units, ISO 8601 dates, withCount envelope shape) |
| 157 | +- HTTP status codes (200 / 201 / 204 / 400 / 404) |
| 158 | +- Filter operator vocabulary (eq / ne / gt / gte / lt / lte / in / like / isNull) + subtype gating |
| 159 | + |
| 160 | +### Tier 2 — idiomatic per port |
| 161 | + |
| 162 | +- Spring `@RestController` annotations vs ASP.NET `MapGet` vs Fastify `app.get` vs FastAPI `@router.get` vs Ktor `route { get { } }` |
| 163 | +- Java `Pageable` vs FastAPI `Query` params vs Spring `@RequestParam` |
| 164 | +- DTO/record shapes (Java records, Kotlin data classes, C# records, Pydantic models, Zod schemas) |
| 165 | +- Error response idiom (Spring `@ControllerAdvice` vs FastAPI exception handlers vs ASP.NET ProblemDetails) |
| 166 | +- Angular vs React component API (Angular standalone components + DI; React hooks + context) |
| 167 | + |
| 168 | +### Tier 3 — internal / free |
| 169 | + |
| 170 | +- Test runner choice per port |
| 171 | +- Dev-server port conventions |
| 172 | + |
| 173 | +## 4. Driving consumers |
| 174 | + |
| 175 | +- **Party-lore-class** (Spring Boot + Kotlin + Exposed + Postgres): wants 2.2 (Kotlin Spring controllers) |
| 176 | +- **C# .NET 8 + Angular 18 adopter** (new addition this FR): wants 2.4 (C# refinement) + 2.5 (Angular client) + a docs recipe |
| 177 | +- **Future TS-only React adopter**: covered today, gets 2.5 Angular as an alternate client option |
| 178 | +- **Future Python/FastAPI adopter**: wants 2.3 |
| 179 | +- **Future Java/Spring adopter**: wants 2.1 |
| 180 | + |
| 181 | +## 5. Out of scope |
| 182 | + |
| 183 | +- **GraphQL** route codegen — REST only Day 1; GraphQL can be a separate FR |
| 184 | +- **WebSocket / SSE** route codegen — REST only Day 1 |
| 185 | +- **Authentication / authorization** middleware — out of scope; consumer wires Spring Security / `[Authorize]` / FastAPI dependencies / Ktor auth around the generated routes |
| 186 | +- **OpenAPI schema generation** — useful but separate; Spring + ASP.NET + FastAPI already emit OpenAPI natively from the generated controllers. Codifying a cross-port OpenAPI Tier 1 contract is a follow-up FR. |
| 187 | +- **Svelte / React Native / Vue clients** — defer; the Angular addition validates that the universal client pattern scales beyond React/TanStack. |
| 188 | + |
| 189 | +## 6. Ordering + dependencies |
| 190 | + |
| 191 | +Each sub-project (2.1–2.6) is independently implementable. Recommended sequence based on likely consumer demand: |
| 192 | + |
| 193 | +1. **2.5 Angular client tier** first — unblocks the C#-Angular adopter immediately (C# routes already ship). Validates the universal-client architecture. |
| 194 | +2. **2.4 C# refinement** — small audit; lands in parallel with 2.5. |
| 195 | +3. **2.2 Kotlin Spring controllers** — party-lore-class consumer. |
| 196 | +4. **2.6 API contract conformance corpus** — once 2+ ports have route codegen, the corpus has something to verify cross-port. |
| 197 | +5. **2.1 Java Spring controllers** — when a Spring/Java consumer surfaces. |
| 198 | +6. **2.3 Python FastAPI routes** — when a FastAPI consumer surfaces. |
| 199 | + |
| 200 | +Each gets its own brainstorm → spec → plan → impl cycle. This FR is the umbrella spec; individual sub-project specs live at `docs/superpowers/specs/2026-XX-XX-fr-008-<sub-project>-design.md`. |
| 201 | + |
| 202 | +## 7. Risks |
| 203 | + |
| 204 | +1. **Angular 18 standalone-components-only is a recent default** — older Angular tutorials reference NgModules. Mitigation: codegen output uses standalone components throughout; recipe documents this explicitly. |
| 205 | +2. **CORS + dev-server proxying** is consumer-specific — Angular dev-server on port 4200, ASP.NET on port 5000, Spring on 8080. Mitigation: per-recipe documentation; not a codegen concern. |
| 206 | +3. **TanStack Angular Table 8.x** maturity — newer than React equivalent. Mitigation: snapshot test the Angular grid output; if TanStack Angular Table doesn't fit, fall back to AG Grid or a hand-rolled table — decision deferred to 2.5 implementation time. |
| 207 | +4. **OpenAPI Tier 1 invariant** absence may let backends drift on response shapes — mitigation is the API contract conformance corpus (2.6). |
| 208 | +5. **Spring controller codegen** (2.1) must not collide with consumers' existing manually-written controllers — mitigation: `@generated` headers + overwrite policy (same as Java codegen-mustache today). |
| 209 | + |
| 210 | +## 8. Versioning + compatibility |
| 211 | + |
| 212 | +- Target: `7.0.0+` for each sub-project as it lands |
| 213 | +- The API contract (URL grammar + wire format) is **frozen Tier 1** — any future change requires bumping the major version and updating every port's runner in lockstep |
| 214 | +- Generator names are Tier 1 invariants per sub-project's spec |
| 215 | + |
| 216 | +## 9. Cross-references |
| 217 | + |
| 218 | +- [`docs/features/api-contract.md`](../../docs/features/api-contract.md) — the cross-port REST contract this FR builds against |
| 219 | +- [`docs/ports/typescript-client.md`](../../docs/ports/typescript-client.md) — the React/TanStack client; Angular adds a peer |
| 220 | +- TS reference: `server/typescript/packages/codegen-ts/src/templates/routes-file.ts` + `runtime-ts/drizzle-fastify` |
| 221 | +- C# reference: `server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs` |
| 222 | +- Kotlin codegen pattern: [`docs/superpowers/specs/2026-05-25-codegen-kotlin-design.md`](2026-05-25-codegen-kotlin-design.md) |
| 223 | +- Java codegen pattern: `server/java/codegen-base/` + `server/java/codegen-mustache/` |
| 224 | +- Universal client architecture: CLAUDE.md "Framework integration: separate codegen and runtime packages" |
0 commit comments