Nine Requests of Thirty-Seven
Inflow ships an executable request collection - five .rest files, thirty-seven requests. On a fresh clone nine of them return a 2xx, and eight of those nine are in one file. This series reads the repository itself as the artefact.
The first five minutes with a sample repository are the only five minutes most readers give it. You clone, you read the run-book, you start the infrastructure, you start the app, and then you look for the thing that proves it is alive — the Postman collection, the curl snippet, the .rest file. What happens next decides whether you keep reading the code or close the tab.
Inflow — the MIT-licensed modular-monolith sample by Piotr Gankiewicz and DevMentors at github.com/devmentors/Inflow — does the right thing here. It ships an executable request collection: five .rest files, thirty-seven requests, in the VS Code REST Client format, and the README promotes them by name. That is more than most sample repositories manage.
Run them on a fresh clone, top to bottom, in the order the files list them, and nine come back with a 2xx.
To be clear up front: I did not write Inflow. I am a source-reader working through someone else's teaching code, and this series is unusual even by that standard — its subject is not a class or a pattern but the repository as an artefact. The README, the .rest files, the solution file, the twenty .csproj files, docker-compose.yml, and the git log across six branches. Where the repository is the story, I quote it and name the branch; everything unqualified below is master, tip 39e96da. Where I criticise, I have checked the code first, and I have taken equal care to record what the repository gets right — which, as part 5 will show, is most of what it claims.
The root file that fails four ways out of five
Inflow.rest sits in the repository root. It is 161 bytes, it is the first request file a newcomer opens, and it is the whole of the estate's front door:
@url = http://localhost:5000
###
GET {{url}}
###
GET {{url}}/customers
###
GET {{url}}/payments
###
GET {{url}}/wallets
###
GET {{url}}/users
No authentication, no preceding sign-in, five bare GETs. Here is what each one does against a freshly migrated database:
| Request | Result | Why |
|---|---|---|
GET / |
200 Inflow API |
Mapped inline in Startup.Configure |
GET /customers |
401 | CustomersController.BrowseAsync carries [Authorize("customers")] |
GET /payments |
404 | There is no PaymentsController |
GET /wallets |
401 | WalletsController carries a class-level [Authorize] |
GET /users |
401 | UsersController carries a class-level [Authorize("users")] |
Four of the five 401s and 404s arrive with no explanatory body, because they are produced by the authentication middleware and the router respectively, not by the application. The reader learns nothing from them except that something is wrong.
Three of those four are the same fact restated: the endpoints are secured, and the file that introduces them sends no credentials. That is a documentation gap, and a small one. The fourth is different in kind, and it gets its own article.
Twenty-two requests, no credentials
The pattern repeats one level down. Three of the four per-module files — Customers.rest, Payments.rest, Wallets.rest — contain twenty-two requests between them, and across all twenty-two there is not one Cookie header, not one Authorization header, and not one Set-Cookie. I checked every [Authorize] attribute on the seven controllers those files call. The only anonymous endpoints in the entire estate are four:
POST /customers— create customerPUT /deposits/{depositId}/complete— commented in the source as “Acting as a webhook for 3rd party payments service”POST /account/sign-upPOST /account/sign-in
Two of those four are the sign-up and sign-in pair, which live in the fourth file. So of the twenty-two credential-free requests, exactly two land on endpoints that would accept them, and both of those fail anyway for reasons that have nothing to do with auth. PUT /deposits/{{depositId}}/complete uses a placeholder GUID and gets deposit_not_found. And POST /customers is the more interesting of the two, because it fails on a fresh clone for a reason nobody would guess from the file:
var user = await _userApiClient.GetAsync(command.Email);
if (user is null)
{
throw new UserNotFoundException(command.Email);
}
CreateCustomerHandler reaches across the module boundary to ask the Users module whether that email belongs to a user. On a clean database it does not, so the request returns a 400 carrying the code user_not_found. And after you have signed up, the customer already exists — the Users module publishes SignedUp, and the Customers module creates the customer for you — so the same request then returns customer_already_exists. There is no state in which POST /customers succeeds along the documented path. It is a staged alternative that lost its stage, and I will come back to it in part 2.
The one file that works
Users.rest is the outlier, and it is the reason this article is a finding rather than a complaint. It is ten requests long and it is self-contained: it signs up, signs in, captures the response, and reuses it.
### Login as the regular user
# @name sign_in
POST {{url}}/account/sign-in
Content-Type: application/json
{
"email": "{{email}}",
"password": "{{password}}"
}
###
@authCookie = {{sign_in.response.headers.$.set-cookie}}
@userId = {{sign_in.response.body.$.id}}
That is REST Client's named-response syntax used properly. Name a request, then bind variables out of its response headers and body. Eight of the ten requests in this file return a 2xx on a first run, including everything from sign-up through to browsing the user list as an administrator. The author clearly knew how to build a request collection that carries its own identity and harvests its own identifiers.
The two that fail do so for one reason. Two thirds of the way down, after @userId has been bound to a real value from the sign-in response, the file overwrites it:
@userId = 00000000-0000-0000-0000-000000000000
GET /users/{{userId}} and PUT /users/{{userId}}/state then address Guid.Empty, which is guaranteed to match no row. A working dynamic binding was replaced by a placeholder, and the two requests that follow it are the only two in the file that fail.
There is a second defect in Users.rest that costs nothing at all, which makes it worth naming precisely: every authenticated request sends Set-Cookie: as a request header. Set-Cookie is a response header; the request form is Cookie. The requests work regardless, because REST Client keeps a cookie jar and re-sends the cookie it captured from the sign-in response on its own. The manual header is inert. It is a real mistake and it has no effect, and being able to say both of those things in one sentence is the standard I want to hold this series to.
The ledger
| File | Requests | Return 2xx cold |
|---|---|---|
Inflow.rest |
5 | 1 |
Customers.rest |
7 | 0 |
Payments.rest |
8 | 0 |
Users.rest |
10 | 8 |
Wallets.rest |
7 | 0 |
| Total | 37 | 9 |
Strip out Users.rest and the number is one of twenty-seven, and that one is GET / returning the string Inflow API.
Why this is structural, not careless
The easy article here is “the sample app is broken”. It is also wrong, and Users.rest is the proof. The author built exactly the collection this repository needed — self-authenticating, self-harvesting, ordered so that each request sets up the next — and then built it once, for the one module whose interesting behaviour is entirely contained within itself.
The other three files fail because the thing they document is not contained within a module. Getting money into a wallet in Inflow crosses Users, Customers, Payments, Wallets and the Saga; it requires two identities; and most of it happens in event handlers that no HTTP request names. There is no per-module file that can describe that, because the unit of documentation here is the module and the subject is the seam between modules.
The repository's organising principle, applied faithfully to its own documentation, is what makes its own subject undiscoverable. That is a much more interesting problem than a broken collection, and it is the thesis of this series.
One more piece of context before the roadmap, because it changes how you should read everything that follows. This is a free, MIT-licensed teaching repository, published in 2021 to accompany a paid course. It has no deployment, no hosted instance, no Dockerfile on any branch, and no CI. Its last commit landed on 23 July 2022. Nothing here was ever shipped to anyone, and criticism that forgets that is worthless. Where the repository made a deliberate simplification I will say so and show where it is recorded; where it drifted I will say that instead; and I will not present one as the other.
Where the series goes
- Nine requests of thirty-seven — this post.
- Fourteen steps and nine of them invisible — the happy path, reconstructed from source, and why no file contains it.
- The route that never existed — a 404 that is simultaneously proof the architecture claim is real.
- Register as the admin user — an escalation path with a caption, stated precisely.
- Eleven claims, and most of them true — the README audited line by line, including the strongest claim in it.
- An afternoon in March — five commits between 11:56 and 16:08, and what they record.
- Two workshops that forked — the same exercise, prepared twice, diverging two weeks apart.
- The upgrade that only moved braces — 519 files of .NET 6 migration, decomposed.
- A guard for the drift, none for the rule — a runtime check for the invariant that can break by accident, and nothing for the one the repository is about.
- Seven choices and one comment — the simplifications, and the single place any of them is written down.
- Four years cold — the retrospective, and what four DevMentors estates in this corpus add up to.
Three companion series read the same repository from the inside: The Framework Underneath takes the shared mini-framework, Five Modules, One Database Each takes the domain code, and One Module Leaves the Process takes the microservices branch. This one stays outside the C# and reads everything around it.
If you have ever cloned a well-regarded sample repository, run its request file, and quietly assumed the 401s were your fault — they were not, and part 2 reconstructs the path the files could not give you.