Skip to content

Product Reviews showcase: StrongTypes end to end#1

Merged
KaliCZ merged 11 commits into
mainfrom
claude/strongtypes-showcase-app-15d67f
Jul 15, 2026
Merged

Product Reviews showcase: StrongTypes end to end#1
KaliCZ merged 11 commits into
mainfrom
claude/strongtypes-showcase-app-15d67f

Conversation

@KaliCZ

@KaliCZ KaliCZ commented Jul 15, 2026

Copy link
Copy Markdown
Owner

The complete showcase + starter template described in the spec docs: a product-reviews platform where every constrained value is a strong type from the request DTO through the domain and PostgreSQL, out into the OpenAPI document, and into the generated TypeScript client.

What is in here

  • Spec-driven docs: docs/business-requirements.md (product terms), docs/technical-requirements.md (architecture + conventions), 8 ADRs (decisions per aspect, immutable once accepted), docs/assumptions.md (everything the brief left open — please review this file first).
  • Backend: ASP.NET Core controllers in vertical slices, DDD entities with behavior, one handler file per operation returning Result<T, TError> with per-operation error enums, EF Core storing strong types directly (UseStrongTypes()), proof-of-loading aggregates, idempotent startup seeding.
  • StrongTypes showcase: no data annotations or guard clauses anywhere; Maybe<T>? three-state PATCH edit; consumer-defined [NumericWrapper] Rating (1–5); Swashbuckle document carries format: email, minLength, exclusiveMinimum, and the rating bounds.
  • Aspire: one command boots Postgres, Zitadel v4 + hosted login (idempotently provisioned PKCE SPA client + demo@productreviews.local / critic@productreviews.local), API, and the Vue 3 frontend.
  • Frontend: Vue 3 + Vite SPA; all API access through the openapi-fetch client generated from the committed openapi.json; drift fails an E2E contract test.
  • Tests (no mocks): 13 FsCheck/xUnit domain tests, 28 wire-level integration tests on Testcontainers Postgres, 16 Vitest tests, 8 Playwright E2E tests that sign in through the real Zitadel form.
  • CI: backend build+tests, frontend format/typecheck/unit/build, full-stack E2E; Dependabot for nuget/npm/actions.

Notes for review

  • docs/assumptions.md lists the judgment calls (Vue+Vite over Nuxt, moderation dropped with Temporal, hard delete, pros/cons fields added for the Maybe demo, own-review voting enforced server-side).
  • Found a StrongTypes generator bug along the way: [NumericWrapper] on a non-generic struct emits Equals(int?)/CompareTo(int?), which fail the generated IEquatable<int>/IComparable<int>Rating.cs carries a two-line workaround; fix belongs in the library repo.
  • Squash-merge if you want main to read as a single initial commit; the branch history is in logical increments if you prefer to keep it.

🤖 Generated with Claude Code

KaliCZ and others added 11 commits July 15, 2026 07:56
Spec-first foundation for the StrongTypes showcase: product-terms business
requirements, the technical design (stack, domain model, API surface,
conventions), eight ADRs covering the pivotal decisions, and a list of
assumptions to validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slnx solution, central package management, Directory.Build.props with
correctness-as-errors editorconfig, ServiceDefaults in its own namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product/Review/ReviewVote with strong-typed properties stored directly via
UseStrongTypes(); custom Rating [NumericWrapper] strong type (1..5) with its
own JSON converter and reused EF converter; one handler file per operation
returning Result<T, TError> with per-operation error enums; proof-of-loading
aggregates (ProductWithReviews, ReviewWithVotes) with their Include chains in
CompleteQueries; idempotent startup seeder building the demo catalog through
the domain entities.

Rating carries two hand-written members (Equals(int)/CompareTo(int)) to work
around a NumericWrapper generator gap with non-generic wrappers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API: feature slices (Catalog, Reviews, Votes, Profile) with strong-typed DTO
records, exhaustive error-enum-to-HTTP mapping in controllers, and one
infrastructure concern per file (Authentication, Persistence, OpenApi,
RateLimits, ErrorHandling, Observability). Swashbuckle carries strong-type
constraints via AddStrongTypes(); the custom Rating maps to minimum/maximum.

AppHost: Postgres (persistent in dev, throwaway in E2E), Zitadel v4 + Login V2
containers, idempotent provisioning of the PKCE SPA client and two demo
reviewers, endpoint/env wiring for the API and the Vite frontend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SPA with catalog, product detail, review submit/edit/delete, and voting.
All API access goes through an openapi-fetch client typed by schema.d.ts,
generated from the committed openapi.json snapshot; the PATCH edit builds
its body via the three-state Maybe contract ({} clears, {Value} sets,
omitted leaves unchanged). Auth is oidc-client-ts redirect PKCE against
Zitadel; the Vite dev/preview server proxies /api to the backend using
Aspire-injected env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n suite

Domain tests (13): Rating bounds/ordering properties, the ApplyEdit three-state
Maybe contract, score arithmetic, and rating-summary aggregation, with a shared
generators class re-exporting the Kalicz.StrongTypes.FsCheck arbitraries.

Integration tests (28): one shared Postgres container + API host per run,
exercising the real migrate/seed startup path; wire-level JsonElement asserts
on anonymous payloads; JWTs minted against a test-owned key through the real
JwtBearer pipeline; covers catalog, filtering/sorting/paging, submit/edit/
delete (incl. the {}/{"Value"} Maybe semantics), voting rules, profile, and
the OpenAPI document constraints. No mocks anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unit (16): the buildEditReviewBody three-state Maybe contract, StarRating
display/input behavior, ReviewCard vote/ownership rules, problem message
extraction. E2E: Playwright boots the full Aspire stack in E2E mode
(throwaway containers, preview build on :4173), signs in through the real
Zitadel hosted login, and walks browse -> review -> edit (clear pros) ->
vote -> delete; a contract spec fails when the committed openapi.json
drifts from the running API (refresh via npm run refresh:api).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aims

- Guard review reloads with a request sequence so rapid sort/filter changes
  cannot apply out of order.
- E2E builds the frontend inside the AppHost resource (serve:e2e): VITE_*
  values bake into the bundle at build time, so the build must run with the
  Aspire-injected env; Playwright no longer pre-builds.
- E2E uses a throwaway Zitadel machine-key dir — a PAT left over from a
  previous run belongs to a dead instance and provisioning would 401.
- Rename the zitadeldb resource (collided with the zitadel container name).
- Profile claims: the SPA gets name via idTokenUserinfoAssertion; the API
  enriches the validated principal from the userinfo endpoint (cached per
  subject) because Zitadel JWT access tokens carry only the subject.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
During a cold first init the PAT file on disk can briefly belong to a previous
instance (Zitadel overwrites it only when init finishes) — a 401 means
"re-read and try again", not "give up".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI mirrors the house pattern: backend build + tests (analyzers fail the
build, Testcontainers integration suite), frontend format/typecheck/unit/
build, and a full-stack Playwright E2E job with cached browsers and trace
upload. Dependabot covers nuget (central package management), npm, and
github-actions, grouped weekly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@KaliCZ
KaliCZ merged commit e052f31 into main Jul 15, 2026
3 checks passed
@KaliCZ
KaliCZ deleted the claude/strongtypes-showcase-app-15d67f branch July 15, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant