<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: luis8choa</title>
    <description>The latest articles on DEV Community by luis8choa (@luis8choa).</description>
    <link>https://dev.to/luis8choa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938330%2F1f5100f2-7b32-4ba3-b8d9-1f7e46e222ae.png</url>
      <title>DEV Community: luis8choa</title>
      <link>https://dev.to/luis8choa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luis8choa"/>
    <language>en</language>
    <item>
      <title>Building a Healthcare RCM Analytics API with FastAPI and PostgreSQL</title>
      <dc:creator>luis8choa</dc:creator>
      <pubDate>Tue, 19 May 2026 01:48:18 +0000</pubDate>
      <link>https://dev.to/luis8choa/building-a-healthcare-rcm-analytics-api-with-fastapi-and-postgresql-3jj5</link>
      <guid>https://dev.to/luis8choa/building-a-healthcare-rcm-analytics-api-with-fastapi-and-postgresql-3jj5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm a biomedical engineer who ended up doing data analysis for a Revenue Cycle Management company in the U.S. — and honestly, I wouldn't change it. There's something fascinating about the intersection of healthcare operations and data.&lt;/p&gt;

&lt;p&gt;But after two years on the floor, one thing became painfully obvious: the people actually resolving claims — billers, coders, AR agents — are flying blind. We're working with spreadsheets, static reports that are already two days old, and gut feeling. When you're processing 60,000+ claims a month, gut feeling doesn't scale.&lt;/p&gt;

&lt;p&gt;The bottlenecks are always the same: which payer is denying the most claims right now? Which claims have been sitting unpaid for 90+ days and are probably never going to be resolved? Who on the team has a 40% error rate that nobody has flagged yet?&lt;/p&gt;

&lt;p&gt;This information exists. It's sitting in the database. Nobody built the tool to surface it.&lt;/p&gt;

&lt;p&gt;So I did. This is the story of building a REST API that gives RCM teams real-time access to the KPIs that actually matter — denial rates, AR days, aging buckets, and staff productivity — using FastAPI and PostgreSQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 1: What is Revenue Cycle Management?
&lt;/h2&gt;

&lt;p&gt;If you're not in healthcare, RCM might sound abstract. Let me make it concrete.&lt;/p&gt;

&lt;p&gt;Every time a patient sees a doctor, the provider needs to get paid. That payment doesn't come directly from the patient in most cases — it comes from an insurance company (called a &lt;strong&gt;payer&lt;/strong&gt;). The process of getting that money from the payer is Revenue Cycle Management.&lt;/p&gt;

&lt;p&gt;The claim cycle looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Medical service rendered
        ↓
Medical coding (CPT codes assigned to each procedure)
        ↓
Claim submitted to payer
        ↓
Payer reviews the claim
        ↓
Paid / Denied / Pending / Appealed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers tell you almost everything about the health of this cycle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Denial Rate&lt;/strong&gt; — the percentage of claims a payer rejects. A high denial rate with a specific payer usually signals a billing accuracy problem, a credentialing issue, or a payer policy change nobody caught. If BlueCross is denying 35% of your claims and Aetna is only denying 8%, that's where your team should be focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AR Days&lt;/strong&gt; — how many days on average it takes from claim submission to payment. The industry benchmark is 40 days. Above that, cash flow starts to suffer. Above 90 days, you're probably not getting paid at all.&lt;/p&gt;

&lt;p&gt;The problem isn't that this data doesn't exist — it does. The problem is that when you're managing 60,000+ claims, nobody has a clean, real-time view of it. Directors and managers are making staffing and process decisions based on reports that are already outdated.&lt;/p&gt;

&lt;p&gt;That's the gap this API fills.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2: Architecture Decisions
&lt;/h2&gt;

&lt;p&gt;When I started thinking about how to build this, I had three requirements: it had to be fast to query, easy for other developers to understand and extend, and it had to separate the business logic cleanly from the HTTP layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why FastAPI over Flask or Django
&lt;/h3&gt;

&lt;p&gt;FastAPI generates interactive API documentation automatically from your code. For a tool that other analysts or developers might consume, that matters enormously — the &lt;code&gt;/docs&lt;/code&gt; endpoint gives you a working Swagger UI with zero extra configuration. Flask would have required a separate library for that. Django would have been overkill for an API-only project.&lt;/p&gt;

&lt;p&gt;FastAPI also validates request and response data automatically using Python type hints, which catches entire categories of bugs before they reach production.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Service Layer Pattern
&lt;/h3&gt;

&lt;p&gt;The most important architectural decision was separating business logic into a &lt;code&gt;services/&lt;/code&gt; layer completely independent from the HTTP routing layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;routers&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;            &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;receives&lt;/span&gt; &lt;span class="n"&gt;HTTP&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;claims_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="n"&gt;calculation&lt;/span&gt; &lt;span class="n"&gt;logic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means two things in practice. First, the alert system can call &lt;code&gt;get_denial_rate()&lt;/code&gt; directly from &lt;code&gt;claims_service&lt;/code&gt; without making an internal HTTP request — it just calls the Python function. Second, testing the denial rate calculation doesn't require simulating an HTTP request — you call the function directly with a database session.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLAlchemy 2.0 with Pydantic v2
&lt;/h3&gt;

&lt;p&gt;SQLAlchemy handles the relationship between Python objects and PostgreSQL tables. Pydantic handles the relationship between Python objects and JSON. They solve different problems and that's why you need both.&lt;/p&gt;

&lt;p&gt;A SQLAlchemy model says: "this is how a claim is stored in the database — all columns, all relationships, including internal fields like &lt;code&gt;hashed_password&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;A Pydantic schema says: "this is what a claim looks like when it enters or leaves the API — only the fields the client should see."&lt;/p&gt;

&lt;p&gt;The separation is what prevents accidentally exposing &lt;code&gt;hashed_password&lt;/code&gt; in a response.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3: The Denial Rate Calculation
&lt;/h2&gt;

&lt;p&gt;This is the query I'm most proud of in the project — not because it's complex, but because it took me a while to realize it could be done in a single database round trip.&lt;/p&gt;

&lt;p&gt;My first instinct was two separate queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Query 1 — total claims per payer
&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Query 2 — denied claims per payer
&lt;/span&gt;&lt;span class="n"&gt;denied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;\
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;\
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two trips to the database. Two result sets to merge in Python. More code, more room for error.&lt;/p&gt;

&lt;p&gt;The better approach uses a conditional &lt;code&gt;SUM&lt;/code&gt; with &lt;code&gt;CASE WHEN&lt;/code&gt; — a single query that counts everything at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;case&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;else_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQL this generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'denied'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;denied&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;payer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CASE WHEN&lt;/code&gt; assigns &lt;code&gt;1&lt;/code&gt; to every denied claim and &lt;code&gt;0&lt;/code&gt; to everything else. &lt;code&gt;SUM&lt;/code&gt; adds those up. One query, one database round trip, all the data you need.&lt;/p&gt;

&lt;p&gt;The denial rate is then calculated in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;denied&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;or 0&lt;/code&gt; handles the edge case where &lt;code&gt;SUM&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt; for payers with zero denials — in SQL, summing an empty set returns &lt;code&gt;NULL&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 4: Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Testing an API that talks to a database introduces a fundamental problem: you can't run tests against your production database, but you also can't test database logic without a database.&lt;/p&gt;

&lt;p&gt;The solution has two parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite for local tests
&lt;/h3&gt;

&lt;p&gt;Instead of requiring a running PostgreSQL server to run tests, the test suite uses SQLite — a file-based database that needs no server. Each test creates all tables, runs, and drops everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;autouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# create tables
&lt;/span&gt;    &lt;span class="k"&gt;yield&lt;/span&gt;                                         &lt;span class="c1"&gt;# run the test
&lt;/span&gt;    &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# drop everything
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every test starts with a completely clean database. No data contamination between tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  FastAPI's &lt;code&gt;dependency_overrides&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The bridge between "my app talks to PostgreSQL" and "my tests talk to SQLite" is FastAPI's dependency override system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dependency_overrides&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;override_get_db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line tells FastAPI: "whenever any endpoint asks for &lt;code&gt;get_db&lt;/code&gt;, give it &lt;code&gt;override_get_db&lt;/code&gt; instead." Every endpoint in the application — without modifying a single line of production code — now talks to SQLite during tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 80% coverage threshold
&lt;/h3&gt;

&lt;p&gt;The CI pipeline fails if test coverage drops below 80%. This isn't about achieving a number — it's about making coverage degradation visible. If someone adds a new service function and forgets to write tests for it, the pipeline catches it before the code reaches main.&lt;/p&gt;

&lt;p&gt;The current coverage is 93%.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Two years working in RCM gave me a very specific frustration. Ten weeks of building gave me a very specific solution.&lt;/p&gt;

&lt;p&gt;The most valuable part of this project wasn't any particular technical decision — it was having to translate operational problems into database queries and vice versa. That translation is hard to fake in an interview, and hard to build without having been on both sides of it.&lt;/p&gt;

&lt;p&gt;The project is open source:&lt;br&gt;
🔗 &lt;a href="https://github.com/luis8choa/RCM-Analytics-API" rel="noopener noreferrer"&gt;github.com/luis8choa/RCM-Analytics-API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's next: a frontend dashboard to visualize the KPIs, and a production deploy so billing teams can actually use it.&lt;/p&gt;

&lt;p&gt;If you work in healthcare operations and this resonates — or if you have feedback on the technical side — I'd genuinely like to hear from you.&lt;/p&gt;

</description>
      <category>rcm</category>
      <category>postgres</category>
      <category>fastapi</category>
      <category>healthcare</category>
    </item>
  </channel>
</rss>
