DEV Community

Cover image for API Calls Done Right: From Messy Fetch to Clean Data Layer

API Calls Done Right: From Messy Fetch to Clean Data Layer

Gavin Cettolo on May 19, 2026

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...
Collapse
 
lucaferri profile image
Luca Ferri

@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?

Collapse
 
gavincettolo profile image
Gavin Cettolo

Not everywhere, but I have seen it in many projects.
Different companies, different stacks, same evolution.

It always begins innocently:

“We just need one quick fetch.”

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 😅

Collapse
 
lucaferri profile image
Luca Ferri

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.

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

Because I wanted developers to understand the responsibilities first.
Libraries come later.

If you understand:

  • transport logic,
  • token management,
  • retries,
  • typed responses,
  • cancellation,
  • domain separation, …then switching from native fetch to Axios or even something like ky becomes trivial.

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

Thread Thread
 
lucaferri profile image
Luca Ferri

The AbortController timeout pattern was a nice touch.
Most tutorials completely ignore hanging requests.
Was that included because of real production incidents?

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

Absolutely, people underestimate how damaging stalled requests are for UX.

Users don’t think:

“Ah yes, the TCP connection probably stalled.”

They think:

“This app is broken.”

Timeouts are one of those invisible quality features.
Nobody notices when they work.
Everybody notices when they don’t.

Collapse
 
syedahmershah profile image
Syed Ahmer Shah

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. 👍

Collapse
 
gavincettolo profile image
Gavin Cettolo

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!

Collapse
 
syedahmershah profile image
Syed Ahmer Shah

Centralized refresh handling and AbortController are exactly what separate production code from tutorial code. Thanks for putting together such a clean, practical guide! 👍

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

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 🙌

Collapse
 
paras594 profile image
Paras 🧙‍♂️ • Edited

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 😂

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thank you @paras594!
If you have any thoughts or suggestions, I'm all ears.

Collapse
 
paras594 profile image
Paras 🧙‍♂️

Sure, will do!

Collapse
 
gavincettolo profile image
Gavin Cettolo

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.

Collapse
 
elenchen profile image
Elen Chen

What common problems did you see when you started refactoring messy fetches in components?

Collapse
 
gavincettolo profile image
Gavin Cettolo

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.

Collapse
 
elenchen profile image
Elen Chen

Why build a separate data layer instead of keeping fetch logic in hooks or components?

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

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.

Thread Thread
 
elenchen profile image
Elen Chen

Can you summarize the pattern you recommend?

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

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.

Collapse
 
neletomartin profile image
Martin

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. 🦄

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thanks a lot @neletomartin, this is such a thoughtful breakdown 🙌

You’re absolutely right about fetch not 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 ApiError approach 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!

Collapse
 
aasteriskz profile image
Adarsh

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!

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thanks @aasteriskz

Collapse
 
leob profile image
leob

Solid! (little "play of words")

Collapse
 
gavincettolo profile image
Gavin Cettolo

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 🚀

Collapse
 
leob profile image
leob

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!

Thread Thread
 
gavincettolo profile image
Gavin Cettolo

Keep me updated :)

Collapse
 
albernaz_ profile image
Beatriz Albernaz

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.

Collapse
 
gavincettolo profile image
Gavin Cettolo

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 fetch calls: 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 🙌