I've seen this file in almost every frontend project I've ever touched.
It's usually called api.js or utils.js or sometimes just helpers.ts.
It s...
For further actions, you may consider blocking this person and/or reporting abuse
@gavincettolo , your article hit a nerve immediately. Especially the part where every project starts with a tiny api.js and six months later nobody wants to touch it anymore.
Have you actually seen that pattern everywhere, or was this inspired by one particularly painful project?
Not everywhere, but I have seen it in many projects.
Different companies, different stacks, same evolution.
It always begins innocently:
Then deadlines arrive.
People duplicate logic.
Auth headers get copy-pasted.
Error handling becomes inconsistent.
Eventually the frontend turns into a distributed network layer nobody understands anymore.
The scary part is that teams normalize it because the app still “works.”
Until scaling starts hurting 😅
That’s exactly what I liked in the article — you framed messy API calls as an architectural debt problem, not just a code style issue.
One thing I’m curious about:
Why did you choose a custom apiClient abstraction instead of recommending Axios directly?
A lot of teams would default to Axios interceptors immediately.
Because I wanted developers to understand the responsibilities first.
Libraries come later.
If you understand:
But if your architecture depends entirely on a library abstraction from day one, developers often never learn why the abstraction exists.
Also, modern fetch is actually pretty capable now.
PS This is another reason: vectra.ai/blog/breaking-down-the-a...
Adding too many ibraries can always be a problem
The AbortController timeout pattern was a nice touch.
Most tutorials completely ignore hanging requests.
Was that included because of real production incidents?
Absolutely, people underestimate how damaging stalled requests are for UX.
Users don’t think:
They think:
Timeouts are one of those invisible quality features.
Nobody notices when they work.
Everybody notices when they don’t.
This is a solid guide. The way you broke down the transition from inline fetch spaghetti to a structured, typed data client with AbortController timeouts and automatic token refreshes is spot on. Many tutorials gloss over error handling and token lifecycle management, so seeing those explicitly decoupled from UI logic is highly valuable. 👍
Thanks a lot @syedahmershah! Really appreciate the thoughtful feedback 🙌
I’m glad the separation of concerns came through clearly, especially around error handling and token lifecycle management. Those are usually the first things that become painful once an app grows, but they often get skipped in simpler examples.
My goal with the article was exactly to show how a small amount of structure in the data layer can make the rest of the app much easier to reason about and maintain over time.
And yes, AbortController + centralized refresh handling have been huge quality-of-life improvements for me in real projects 😄
Thanks again for taking the time to read and comment!
Centralized refresh handling and AbortController are exactly what separate production code from tutorial code. Thanks for putting together such a clean, practical guide! 👍
That’s a great way to put it “production code vs tutorial code” is exactly the gap I wanted to address with the article 🙂
A lot of examples stop at “it works”, but in real applications the hard part is usually everything around the request itself: cancellation, retries, auth state, consistency, error propagation, and keeping UI components free from networking concerns.
Glad you found the guide practical and grounded in real-world needs. Thanks again for the thoughtful feedback 🙌
That's a great article!! 💯💯💯
It makes things easy to read and understand with this way of structuring, also changes to code are predictable and non threatening 😂
Thank you @paras594!
If you have any thoughts or suggestions, I'm all ears.
Sure, will do!
Some of you may have noticed that localStorage is vulnerable to XSS attacks and that tokens should be stored in httpOnly cookies.
This is absolutely correct!
localStorage is used here for simplicity; in production, httpOnly cookies are the most secure choice.
Worth a dedicated article on auth security.
What common problems did you see when you started refactoring messy fetches in components?
I found duplicated logic, inconsistent error handling, tangled concerns (UI vs data fetching), and tests that were hard to write. Components became large and brittle because they managed both rendering and data access.
Why build a separate data layer instead of keeping fetch logic in hooks or components?
A data layer centralizes API contracts and caching, enforces a single source of truth, and makes components simpler and easier to test. Hooks still have a role for integration, but the data layer handles request construction, normalization, and error mapping.
Can you summarize the pattern you recommend?
Split responsibilities: an API client for low-level HTTP, a repository/service layer for business-specific requests and response shaping, and lightweight hooks or connectors for component use. Add a consistent error model and optional caching at the data layer.
This is a clean progression — building up one responsibility at a time makes it easy to follow, and the fetch not throwing on 4xx/5xx point is one of those things that bites everyone exactly once. The typed ApiError getters beat the string-matching mess most codebases end up with.
One addition: for production auth, refresh token in an httpOnly cookie + access token in memory survives XSS better than localStorage and costs you nothing but a re-auth on hard refresh. (Saw you already flagged the cookie point in the comments — good call.)
Honestly my setup is close to this, and you nailed the real lesson in the final thoughts: doing the steps in order is what makes it work. Great series. 🦄
Thanks a lot @neletomartin, this is such a thoughtful breakdown 🙌
You’re absolutely right about
fetchnot throwing on 4xx/5xx responses. That’s one of those JavaScript “gotchas” everyone eventually learns the hard way 😄And I completely agree on the auth strategy. Using an httpOnly refresh token cookie with the access token kept in memory is definitely the safer production-grade approach, especially from an XSS perspective. I wanted the article to stay focused on the data layer architecture itself, but I’m glad you highlighted that nuance here.
Also happy the
ApiErrorapproach resonated with you, I’ve seen too many codebases drift into brittle string matching over time, so having typed guards/helpers has saved me a lot of debugging pain.Really appreciate the kind words about the progression too. That was actually the main challenge while writing this: making each abstraction feel justified instead of “enterprise for the sake of enterprise” 😄
Thanks again for reading and sharing your experience!
This is an excellent guide on structuring API calls. Moving from messy fetch to a clean data layer is a game-changer for maintainability. Thanks for sharing these insights!
Thanks @aasteriskz
Solid! (little "play of words")
Haha, I’ll take the pun and the compliment 😄
Glad you enjoyed it, hopefully the architecture stays “solid” even after a few production deadlines hit 🚀
Yeah this is a pretty cool structure/design ...
I did an API interface in React (with Typescript) some time ago, and I did implement a few aspects/pieces of what you described, but certainly not all of it, only a small part - and I didn't really type my API properly, all the "fields" went in/out as strings (I did use structs/types but used "string" for all the props/attributes) ...
Had been planning to redesign/refactor that for a long time - I'm probably gonna use your design when I do!
Keep me updated :)
Great article!
(to disclose) Co-founder of Faultline Security here.
Worth noting: a centralized data layer isn't just cleaner, it's also where you can enforce security concerns in one place.
Thanks a lot @albernaz_! And absolutely, that’s a really important point.
A centralized data layer becomes the perfect place to enforce cross-cutting concerns consistently: auth handling, request validation, retry policies, rate limiting, logging, telemetry, even security-related protections like CSRF handling or token rotation logic.
That’s actually one of the biggest long-term advantages over scattered inline
fetchcalls: you gain a single control surface for behavior that should stay consistent across the entire app.Really appreciate you adding that perspective, especially coming from the security side of things 🙌