<?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: Ismail ZAHIR</title>
    <description>The latest articles on DEV Community by Ismail ZAHIR (@ismailzahir).</description>
    <link>https://dev.to/ismailzahir</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047894%2Fd92e0840-d13b-44ec-93ad-cf01d8600704.jpg</url>
      <title>DEV Community: Ismail ZAHIR</title>
      <link>https://dev.to/ismailzahir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ismailzahir"/>
    <language>en</language>
    <item>
      <title>Why I Validate Angular Compatibility Using the Published npm Package (Not the Source Code)</title>
      <dc:creator>Ismail ZAHIR</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:52:23 +0000</pubDate>
      <link>https://dev.to/ismailzahir/why-i-validate-angular-compatibility-using-the-published-npm-package-not-the-source-code-808</link>
      <guid>https://dev.to/ismailzahir/why-i-validate-angular-compatibility-using-the-published-npm-package-not-the-source-code-808</guid>
      <description>&lt;p&gt;I just published a new article on a lesson I learned while maintaining my open-source Angular library.&lt;/p&gt;

&lt;p&gt;I used to validate compatibility against my workspace and &lt;code&gt;dist/&lt;/code&gt;, but eventually realized that wasn't what developers actually install from npm.&lt;/p&gt;

&lt;p&gt;That led me to redesign the CI pipeline to validate the &lt;strong&gt;published package&lt;/strong&gt; across multiple Angular versions instead.&lt;/p&gt;

&lt;p&gt;The article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why testing &lt;code&gt;dist/&lt;/code&gt; isn't enough&lt;/li&gt;
&lt;li&gt;Angular partial compilation and the linker&lt;/li&gt;
&lt;li&gt;Why type-checking alone can miss compatibility issues&lt;/li&gt;
&lt;li&gt;Validating the packaged artifact with &lt;code&gt;npm pack&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Building a compatibility matrix for Angular 17–22&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain an Angular library (or any npm package), I'd love to hear how you approach compatibility testing.&lt;/p&gt;

&lt;p&gt;📖 &lt;em&gt;Why I Validate Angular Compatibility Using the Published npm Package (Not the Source Code)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@ismailzahir/why-i-validate-angular-compatibility-using-the-published-npm-package-not-the-source-code-32421e090284?sharedUserId=ismailzahir" rel="noopener noreferrer"&gt;Medium link&lt;/a&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>cicd</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Stopped Copy-Pasting the Same Angular ApiService. Here’s What I Built Instead</title>
      <dc:creator>Ismail ZAHIR</dc:creator>
      <pubDate>Sun, 26 Jul 2026 12:16:23 +0000</pubDate>
      <link>https://dev.to/ismailzahir/i-stopped-copy-pasting-the-same-angular-apiservice-heres-what-i-built-instead-1o3f</link>
      <guid>https://dev.to/ismailzahir/i-stopped-copy-pasting-the-same-angular-apiservice-heres-what-i-built-instead-1o3f</guid>
      <description>&lt;p&gt;Every Angular project I've worked on had an &lt;code&gt;api.service.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Different repository.&lt;/p&gt;

&lt;p&gt;Different company.&lt;/p&gt;

&lt;p&gt;Same file.&lt;/p&gt;

&lt;p&gt;It always started as a thin wrapper around &lt;code&gt;HttpClient&lt;/code&gt;, then gradually accumulated more responsibility: versioning, authentication, retry logic, loading indicators, error handling.&lt;/p&gt;

&lt;p&gt;By the fourth project, I wasn't writing it anymore.&lt;/p&gt;

&lt;p&gt;I was copying it.&lt;/p&gt;

&lt;p&gt;And not carefully. I'd paste the file in, then spend the next hour re-fixing the same three bugs I'd already fixed in the last project — because I'd never written them down anywhere, just patched them in place and moved on.&lt;/p&gt;

&lt;p&gt;That was the moment I realized it wasn't repeated code anymore.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A pattern repeated four times isn't a pattern anymore. It's a missing dependency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I extracted it, made the hard-coded parts configurable, and published it as &lt;code&gt;@ismailza/ngx-api-client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article isn't really about the library. It's about the four decisions that turned a file I was slightly embarrassed to copy-paste into something I'd defend in a code review.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual gap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;HttpClient&lt;/code&gt; is a good HTTP client. That's genuinely all it claims to be, and people keep being disappointed that it isn't more.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpClient&lt;/code&gt; gives you a request. It does not give you a policy.&lt;/p&gt;

&lt;p&gt;Every application still has to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where does the base URL come from, and how does the API version get into it?&lt;/li&gt;
&lt;li&gt;What turns a failed response into something a component can actually render?&lt;/li&gt;
&lt;li&gt;Which requests are safe to retry, and how long do you wait before you do?&lt;/li&gt;
&lt;li&gt;How does a global progress bar find out that anything is in flight?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most applications answer those questions inside an &lt;code&gt;ApiService&lt;/code&gt; that grows organically over time until nobody wants to touch it anymore. I wanted those decisions to live in one place — and to see, at a glance, how a request actually moves through the system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl55i6zfvess3wfftdpnw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl55i6zfvess3wfftdpnw.png" alt="How a request actually moves through ngx-api-client&lt;br&gt;
" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A request leaves the component, passes through ApiService, then through an ordered chain — your auth interceptor, retry with jitter, your backend, error normalization, an optional success step — before looping back. Loading state and error handling hang off that chain as pluggable pieces, not built-in opinions.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The first mistake I kept repeating was hard-coding the version
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v1&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It works...&lt;/p&gt;

&lt;p&gt;...until your backend decides to version using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;headers&lt;/li&gt;
&lt;li&gt;query parameters&lt;/li&gt;
&lt;li&gt;media types&lt;/li&gt;
&lt;li&gt;dates&lt;/li&gt;
&lt;li&gt;or no URL versioning at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of treating versioning as string concatenation, I modeled it as a configurable strategy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;provideApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&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;span class="na"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same configuration can instead produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api/v2/orders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/orders?v=2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-API-Version: 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Accept: application/vnd.api.v2+json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without changing application code.&lt;/p&gt;

&lt;p&gt;Two rules became surprisingly important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A header or query parameter the caller set explicitly is never overwritten by the versioning strategy.&lt;/strong&gt; If you pass &lt;code&gt;Api-Version&lt;/code&gt; yourself, you meant it. A library that silently clobbers explicit caller input is a library you cannot debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versions should accept strings as well as numbers.&lt;/strong&gt; This only became obvious after integrating with a backend that versioned by date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;provideApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-01-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Api-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I had typed it as &lt;code&gt;number&lt;/code&gt; — which is what my copy-pasted version implicitly assumed — the whole abstraction would have been useless for the exact API that motivated making it configurable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I realized my biggest problem wasn't requests — it was failures
&lt;/h2&gt;

&lt;p&gt;HTTP failures rarely look the same.&lt;/p&gt;

&lt;p&gt;Sometimes you get a proper RFC 9457 Problem Details response. Well-behaved, easy.&lt;/p&gt;

&lt;p&gt;Sometimes nginx sends back plain text, because it answered before your app ever saw the request.&lt;/p&gt;

&lt;p&gt;Sometimes the network disappears completely and the status is simply &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Components shouldn't need three different code paths for those three situations. Everything gets normalized into one shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// RFC 9457 problem type URI&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 'Bad Request'&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0 for a network failure&lt;/span&gt;
  &lt;span class="nl"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// safe to show the user&lt;/span&gt;
  &lt;span class="nl"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// path that produced it&lt;/span&gt;
  &lt;span class="nl"&gt;code&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// machine-readable, e.g. 'VALIDATION_ERROR'&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// from the body, else the X-Trace-Id header&lt;/span&gt;
  &lt;span class="nl"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a component receives this, it no longer cares where the failure originated.&lt;/p&gt;

&lt;p&gt;Two things worth stating out loud:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch on &lt;code&gt;code&lt;/code&gt;, never on &lt;code&gt;detail&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;detail&lt;/code&gt; is prose meant for a human. It gets reworded, translated, and A/B tested. The day somebody changes "Invalid credentials" to "Incorrect email or password," every &lt;code&gt;if (error.detail === "Invalid credentials")&lt;/code&gt; in your codebase silently stops working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVALID_CREDENTIALS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;code&lt;/code&gt; is the contract. &lt;code&gt;detail&lt;/code&gt; is for humans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;traceId&lt;/code&gt; falls back to the &lt;code&gt;X-Trace-Id&lt;/code&gt; header.&lt;/strong&gt; Nine times out of ten the useful correlation ID is in the response headers and the error body is empty. If your error model discards headers, every production bug report starts with "can you reproduce it?"&lt;/p&gt;




&lt;h2&gt;
  
  
  I almost shipped a toast notification. I'm glad I didn't
&lt;/h2&gt;

&lt;p&gt;That was the easiest feature to add, and the hardest to leave out on purpose.&lt;/p&gt;

&lt;p&gt;The moment a library renders anything, it has an opinion about your design system, your i18n setup, and your accessibility strategy — three things it cannot possibly know. So the library only exposes an error handler that applications replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToastApiErrorHandler&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ApiErrorHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;toast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MyToastService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP layer reports problems. The application decides how users experience them.&lt;/p&gt;

&lt;p&gt;The practical payoff: peer dependencies are &lt;code&gt;@angular/core&lt;/code&gt;, &lt;code&gt;@angular/common&lt;/code&gt;, and &lt;code&gt;rxjs&lt;/code&gt;. Nothing else. A library sitting in your HTTP layer has no business pulling a UI kit into your bundle.&lt;/p&gt;

&lt;p&gt;One honest caveat I put in the README rather than hiding: the default handler passes the whole &lt;code&gt;ApiError&lt;/code&gt; to Angular's own &lt;code&gt;ErrorHandler&lt;/code&gt;, which logs it. If your API puts sensitive data in &lt;code&gt;detail&lt;/code&gt; or &lt;code&gt;instance&lt;/code&gt;, register your own handler instead of relying on the default. Defaults should be safe, and where they can't be, they should say so.&lt;/p&gt;




&lt;h2&gt;
  
  
  One decision surprised every reviewer — I don't register my own interceptors
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;provideHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;withInterceptors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;retryInterceptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;apiErrorInterceptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;apiSuccessInterceptor&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one I had to defend hardest in review. At first glance it looks like boilerplate the library should absorb. It isn't, because order matters and only you know what else is in the chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An auth interceptor has to come first, so a retried request picks up a fresh token instead of replaying the expired one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retryInterceptor&lt;/code&gt; has to come before &lt;code&gt;apiErrorInterceptor&lt;/code&gt;, so your error handler only ever sees failures that survived every retry. Get this backwards and users see three toasts for one eventually-successful request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the library silently injected interceptors at a position it chose, that position would be wrong in some app, and debugging it would mean reading my source. Making the ordering explicit costs one line and buys the ability to reason about the chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retry is harder than it looks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's five characters and it isn't enough. A few rules that all turned out to matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't retry &lt;code&gt;POST&lt;/code&gt; requests by default. It isn't idempotent — replaying it can create two orders. Callers opt in per request when they know their endpoint is safe.&lt;/li&gt;
&lt;li&gt;Respect the server's &lt;code&gt;Retry-After&lt;/code&gt; header. On a 429, the server has told you exactly when to come back; your backoff curve is a guess, the header isn't.&lt;/li&gt;
&lt;li&gt;Retry only transient failures: &lt;code&gt;408, 429, 500, 502, 503, 504&lt;/code&gt;. Retrying a &lt;code&gt;400&lt;/code&gt; is just asking the server to reject you four times.&lt;/li&gt;
&lt;li&gt;Use exponential backoff — and always add jitter.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exponentialDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryIndex&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exponentialDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exponentialDelay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without jitter, every client that hit your struggling backend at the same moment retries at the same moment, and again two seconds later, and again four seconds after that. You've built a synchronized load test against a server that's already failing. A little randomness breaks the lockstep.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two smaller decisions I'm glad I made
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Per-request configuration travels through &lt;code&gt;HttpContext&lt;/code&gt;, not service state.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LegacyOrder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;version&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/analytics/event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;skipErrorHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;showLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request can disable retries, skip global error handling, or bypass loading indicators without touching any concurrent request. A mutable flag on the service that the next interceptor reads would be a race condition waiting for two calls to happen at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loading state is a counter, not a boolean.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiLoadingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;activeRequests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activeRequests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A boolean flickers: three requests start, the fastest one finishes, and the progress bar disappears while two are still running. A counter with a &lt;code&gt;computed()&lt;/code&gt; signal doesn't. It's a five-line class, and it's the single most-copied snippet from every version of this file I ever wrote.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would tell myself before starting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't extract after the first implementation. Extract after the fourth.&lt;/strong&gt; Three implementations taught me which parts actually varied — the version transport, the error presentation — and which parts never did. Abstracting after one project would have produced configuration options nobody needed, and hard-coded the things that mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the README before the code you're unsure about.&lt;/strong&gt; Every section I struggled to explain was a section where the API was wrong. The interceptor-ordering paragraph took three rewrites, and the third is what convinced me not to auto-register them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be honest about compatibility.&lt;/strong&gt; Mine says: developed and tested against Angular 21; the declared floor of &lt;code&gt;&amp;gt;=17&lt;/code&gt; reflects the APIs used — signals and functional interceptors — rather than a range the CI matrix currently covers. Less impressive than claiming full 17–21 support. Also true, which matters more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Open source isn't about writing perfect software. It's about taking something you've solved the same way four times, pulling out the pattern, and inviting other people to tell you where it's wrong.&lt;/p&gt;

&lt;p&gt;I'm not sure this library is done evolving. But the ideas behind it — versioning as a strategy instead of string concatenation, one error shape no matter what the backend sends back, keeping presentation out of the HTTP layer, and making interceptor order something you choose rather than something the library hides from you — are decisions I'd make again.&lt;/p&gt;

&lt;p&gt;I'm curious how other Angular teams handle this. Do you build your own API layer, or lean directly on &lt;code&gt;HttpClient&lt;/code&gt;? Which of these decisions would you have made differently?&lt;/p&gt;

&lt;p&gt;This is my first technical article, and I'd genuinely love feedback. If you think one of these design decisions is wrong — or you've solved the problem differently — I'd enjoy hearing your perspective.&lt;/p&gt;

&lt;p&gt;The library is on npm as &lt;code&gt;@ismailza/ngx-api-client&lt;/code&gt;, and the source is on GitHub if you'd like to explore the implementation.&lt;/p&gt;

&lt;p&gt;— Ismail&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>software</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
