Ulric
Book a call

Eugene, Oregon · one person, whole builds

Case study

Oregon Provider Finder

A map-first search for Oregon's licensed clinicians, built in C# and Angular on the federal NPI registry. Misspell a city and it still finds the match; ask in a sentence and the server turns it into filters, with or without an AI key.

ClientOpen source · my own build
Year2026
ScopeC#/.NET 10 · ASP.NET Core Web API · Angular · TypeScript · PostgreSQL · Leaflet · Playwright · GitHub Actions
Codegithub.com/erichers/oregon-provider-finder ↗
Oregon Provider Finder
64,780Oregon clinicians, from 9.8 million registry rows
Under 2 minto import the 1.16 GB federal file
0API keys needed to run it

I'm a licensed counselor in Oregon, and I had already built a statewide provider directory in PHP. The question people brought to it was always narrower than the product around it: who practices here, in this specialty, near this place? So I rebuilt that core as a separate app in C# and Angular, against the federal NPI registry, and kept every layer small enough to read in an afternoon.

The data starts as the monthly NPPES dissemination from CMS, a 1.16 GB zip. A .NET importer streams all 9.8 million rows and keeps the 64,780 clinicians with an Oregon practice address and an active NPI. Each lands in one of seven groups from the NUCC taxonomy. The full pass takes under two minutes and peaks at 63 MB of memory. A second file adds 13,545 other practice locations, and every profile names its source and the date of the data.

An ASP.NET Core Web API on .NET 10 serves search and provider records from PostgreSQL 18 through EF Core. Postgres trigrams let a misspelled name or city still find its match. A word search with no place sorts by relevance; add a place and it sorts by distance, in statute miles from Census ZIP centroids. The API connects as a database user that can only read.

The Angular front end is standalone components and signals, with no zone.js. Search happens on a full-window Leaflet map over OpenStreetMap tiles. On a wide screen the results sit in a drawer that collapses; on a phone they are a bottom sheet with three stops. Panning the map searches the area in view. The 988 crisis line stays on screen the whole time.

Plain-words search reads a sentence like "a nurse practitioner in Salem who takes Aetna" and proposes filters. The server checks each one and drops what the registry can't answer, insurance included. With free keys it tries four model providers in turn. Without any, a rules-based reader does the job, so anyone can clone the repo and run it. Keys live in .NET user-secrets and never touch git.

The tests cover each layer: .NET unit tests for the core and the API (the API suite runs against a real Postgres test database), Angular unit tests in Vitest, and Playwright browser tests that include an axe accessibility check. GitHub Actions runs all of it on every pull request, along with a formatting check and a secret scan. Two scripts, setup.sh and dev.sh, take a fresh clone to a running app. The code is public on GitHub.

A real search, recorded: counselors in Eugene, a cluster opened, and my own profile
The app walked through: map, results, a profile, and plain-words search
The whole state on first load: a full-window map, with the list in glass cards on the left
The whole state on first load: a full-window map, with the list in glass cards on the left
Searching Salem: 241 providers, the map zoomed in, and clusters where people share a ZIP center
Searching Salem: 241 providers, the map zoomed in, and clusters where people share a ZIP center
A cluster opened: the 36 providers at that point, with a chip to clear the selection
A cluster opened: the 36 providers at that point, with a chip to clear the selection
A card opens a preview in place, with a link through to the full profile
A card opens a preview in place, with a link through to the full profile
A profile: two specialties, the Corvallis practice, and the other locations the NPPES location file adds
A profile: two specialties, the Corvallis practice, and the other locations the NPPES location file adds
Plain words, "a nurse practitioner in Salem who takes Aetna": three filters set, and a note that the registry doesn't record insurance
Plain words, "a nurse practitioner in Salem who takes Aetna": three filters set, and a note that the registry doesn't record insurance
"counselors in Portand" still finds Portland, thanks to Postgres trigrams
"counselors in Portand" still finds Portland, thanks to Postgres trigrams
Filters open: sort, city or ZIP, radius, and provider type
Filters open: sort, city or ZIP, radius, and provider type
About the data: which federal file every record comes from, and how old it is
About the data: which federal file every record comes from, and how old it is
On a phone the map fills the screen, and the list shows as a short peek at the bottom
On a phone the map fills the screen, and the list shows as a short peek at the bottom
The sheet pulled up: cards over the frosted map
The sheet pulled up: cards over the frosted map
A profile on a phone: tap to call, or get directions
A profile on a phone: tap to call, or get directions

Common questions

What is the stack?

C# on .NET 10 with an ASP.NET Core Web API and EF Core, PostgreSQL 18, and an Angular front end in TypeScript with a Leaflet map. Tests run in xUnit, Vitest, and Playwright, and GitHub Actions checks every pull request.

Where does the data come from?

The federal NPPES file from CMS, the source of every NPI. The importer keeps active clinicians with an Oregon practice address and records the date of the data on every profile.

Does it need an AI key to run?

No. Plain-words search tries a chain of model providers when keys are present and falls back to a rules-based reader when they are not. Keys stay in .NET user-secrets, outside the repo.

Is it live?

It runs locally. The repository is public, and two scripts, setup.sh and dev.sh, take a fresh clone to a working app on ports 5080 and 4200.

← All work