Establish the Minimal API endpoint structure (task 57) #16
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/api-structure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Feature-grouped
MapGroupextension methods perCLAUDE.md, withGET /healthas the worked example.Program.csgets one line per feature and no route patterns.15 tests pass; build, Release build and format all clean. Also run for real:
/health→ 200,POST /health→ 405,/missing→ 404.Choices worth challenging
No aggregator method.
Program.csgrowing one line per feature reads as a table of contents; indirection with one entry buys nothing. One-file change if you disagree.No contracts example.
/healthreturns an empty 200, so no wire type was invented — aHealthResponsewould be a shape task 63 redefines. ADR-0009's boundary is better demonstrated by the first real feature endpoint. This is the one acceptance-criteria-adjacent thing the example does not show.A group rather than bare
MapGetcalls, because the group is where a per-feature authorisation policy, filter or rate limit is declared once and inherited.Left room for
Versioning (62) — only the feature's own segment is written in a feature file, so
/api/v1becomes a parent group. Exception handling (58), CORS (61), rate limiting (123) attach inProgram.cs. Health checks (63) replace the one method body. OpenAPI (60) —TypedResultsalready carries the response type.UseHttpLogginguntouched and still outermost. Adding/healthdoes not make ADR-0015's logged-as-200 defect reachable — the route returns a constant and cannot throw.Verdict: mergeable
Independent review, in a detached worktree at
c3a8b4e. Nothing blocking; the notes below are all optional, and none of them needs to be actioned before merge.What I ran
dotnet build— succeeded, 0 warnings (withTreatWarningsAsErrors).dotnet build -c Release— succeeded, 0 warnings.dotnet test— 15 passed, 0 failed (4 Api, 1 WebUI, 10 Infrastructure, Docker present).dotnet format --verify-no-changes— clean.GET /health→ 200 withContent-Length: 0,POST /health→ 405,GET /missing→ 404. All three claims hold. AlsoGET /health/→ 200,HEAD /health→ 405, and oneHttpLoggingrecord per request including the 405s and the 404.Acceptance criteria
All three are met.
/healthreturns 200 from a running process, not only from a test host. Routes live inHealth/HealthEndpoints.csbehind an extension method andProgram.cscontains no route pattern. The dependency direction is untouched and still correct (Api → Infrastructure → Domain,Api → Contracts), and the ticket's DI wiring already landed withAddPlaceMarkDatabase.The missing contracts example is not a gap. The ticket never mentions contracts, and ADR-0009 decided that
PlaceMark.Contractsships empty and gains types alongside the endpoints that need them. AHealthResponseinvented here would be a wire type with no consumer whose shape ticket #63 immediately redefines — it would demonstrate the boundary by violating the reason the boundary was drawn that way. The rule is stated in the new README section and in the dependency-rules section above it, which is as much as review can rely on given ADR-0009's own admission that nothing in the build enforces it.No aggregator
Right call. One line per feature in
Program.csis the table of contents; an aggregator would be a second file to open to find out which features exist, and it buys nothing until the list is long enough to be unreadable — which at PlaceMark's feature count it will not be. It also stays reversible: the day it is wanted, it is one file and ased.Is the pattern unambiguous?
Yes, for the case it covers. The feature file states the rule twice over (in prose and by example), and the README sketch is close enough to compile after the ellipsis is filled in. A contributor adding
PlacesEndpointswould get the namespace, the folder, the accessibility, the group and the prefix-ownership rule right without asking. Two small things it does not answer, both cheap to settle when the first real feature lands rather than now:Program.cscallsAddPlaceMarkDatabasedirectly, so the first feature needing a scoped service has to decide between another line inProgram.csand anAddPlacesFeature()besideMapPlacesEndpoints(). The endpoint half of the convention is settled; the registration half is not.Program.csdiscards it on the only call site. Harmless, and conventional, but it is a claim nothing in the repository demonstrates.Room for the follow-ups
I checked the forward-compatibility claims rather than taking them, by rebuilding the same group shape in a throwaway app and dumping the route patterns:
var v1 = app.MapGroup("/api/v1"); v1.MapHealthEndpoints();yields/api/v1/healthwith no feature file changing, becauseRouteGroupBuilderis itself anIEndpointRouteBuilder.MapHealthChecks("/")works inside the group and inside a nested version group; it also answersHEAD, which the currentMapGetdoes not (HEAD /healthis 405 today — irrelevant for a container probe, which GETs, but worth knowing if anything polls it withHEAD)./health/—MapGroupjoins with a separator, andMapGet("")produces the same — so the comment's "/healthitself, not/health/" is true of matching but not ofRoutePattern.RawText. I checked whether that leaks: withAddOpenApi/MapOpenApi, the document's path keys come out as/healthand/api/v1/health. So the claim is true everywhere a consumer can see it, and no change is warranted.Program.csor on a group without touching a feature file, as claimed.One thing to remember rather than fix: when 62 lands,
/healthshould stay outside the version group. A liveness URL that moves to/api/v2/healthbreaks every probe configured against it, and versioning an endpoint with no body to version buys nothing. TheProgram.cscomment currently reads as though a version prefix wraps all of these calls; that is the natural reading and it is the wrong outcome for this one feature.Logging
UseHttpLoggingis untouched and still first. The claim that/healthcannot make ADR-0015's logged-as-200 defect reachable is correct: the delegate returns a constantTypedResults.Ok(), takes no parameters to bind and touches no service, so there is no user code in the request path that can throw. The 404 and 405 are produced by routing inside the logging middleware, so they are logged truthfully — I confirmed both in the console output.Side effect worth a thought at #63 rather than here: every probe now writes a request record, and ADR-0015 has no sampling and one sink. A monitor polling every few seconds turns the console into health noise, which is a real cost for logs that record says are meant to be read.
Tests
Conventions are followed:
<ClassUnderTest>Tests,<MethodName>_<Scenario>_<ExpectedResult>, Shouldly through the global using, no?.in front of aShould…, AAA separated by blank lines, and the class sits in aHealth/folder mirroring the feature. The single test asserts the acceptance criterion and nothing more, which is right — 404 and 405 are framework behaviour and testing them would pin someone else's contract.One nit: unlike
RequestLoggingTests, this factory does not pin the environment, so it runs under whateverASPNETCORE_ENVIRONMENTthe machine has — a different middleware pipeline (the developer exception page) on a developer's box than in CI, for no benefit. Its comment also asserts an isolation property (TEST-NET-1, "a request that reached PostgreSQL would hang") that depends onUseSettingoutranking user secrets. It does — I measured it,UseSettingbeatsappsettings.Development.jsonfor the same key — so the comment is true, but it is true by a precedence rule the test does not state and does not need.webHost.UseEnvironment(Environments.Production)would make both points moot and match the file next door.Not required, but considered
No ADR. Correct by ADR-0001's own test:
CLAUDE.mdalready mandates feature-groupedMapGroupextension methods, so the architecturally significant part was decided before this branch; what is left — no aggregator, a group for a single route — is cheap to reverse and now documented where a contributor will actually meet it.