Last time we talked about Axios vs Fetch and how productivity wins over preference.
Now let’s take that a step further — how do you structure your requests so they stop being random lines of code all over your project?
Let’s be real.
Most apps start with a few fetch calls.
Then someone adds Axios.
Then you have five files doing the same thing differently.
Headers here, interceptors there, random try/catch everywhere.
Bam — you’re debugging your own chaos...
So the next logical move is to stop thinking about “which library” and start thinking “what’s my API layer supposed to do?”
One Layer to Rule Them All
Your API layer should have a few clear jobs:
- make requests
- handle errors
- manage tokens
- keep types clean
- stay easy to read
You want a single place that knows how your app talks to the outside world.
Not magic, not complex — just consistent.
Here’s a simple example using Axios:
import axios, { AxiosInstance } from 'axios'
const api: AxiosInstance = axios.create({
baseURL: 'https://my-api.com',
headers: {
'Content-Type': 'application/json'
}
})
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
Done.
Every request now runs through this logic.
No more repeating headers in every file or crazy guessing which request forgot the token.
Types Are Your Safety Net
When you’re using TypeScript, you can make your API smarter.
Define the shape of your data once — and let the compiler yell when something’s off.
interface User {
id: number
name: string
email: string
}
const getUsers = async (): Promise<User[]> => {
const { data } = await api.get<User[]>('/users')
return data
}
Now every part of your app knows what a User
looks like.
No more undefined
surprises later.
Catch Once, Handle Everywhere
The next mess we create as devs is with error handling.
You probably have 10 different try/catch
blocks doing almost the same thing.
Centralize that.
api.interceptors.response.use(
(response) => response,
(error) => {
const message = error.response?.data?.message || 'Something went wrong'
console.error(message)
return Promise.reject(error)
}
)
This way, you log or format errors in one spot — not all over the codebase.
You can even send them to a toast system or a log service later.
Stop Copying, Start Importing
If you find yourself writing new API files that look like the old ones, stop.
Make one folder for all requests.
src/
└── api/
├── client.ts
├── users.ts
├── products.ts
└── types.ts
Every file just imports the same api
instance.
You keep things clean, and onboarding someone new becomes painless.
Some Thoughts
If your app grows, this layer grows with it.
Want retries? Add them once.
Need a loading indicator? Wrap the requests.
Switch base URLs? One line change.
The goal is not to build something fancy — it’s to stop writing the same thing twice.
Where This Fits in 2025... I'm a bit in the past when it comes to coding... you know I don't like breaking things.
If you’re using Next.js or React Server Components, you’ll probably use the built-in fetch
for server data — it’s faster and edge-ready.
But an Axios client like this still makes sense for:
- client-side calls that need auth tokens
- Node services or microservices
- scripts and RPA jobs that hit APIs directly
You can even plug this Axios client into React Query or TanStack Query and let it handle caching, retries, and background refresh.
That’s where productivity really kicks in.
Wrapping Up
Axios or Fetch — it doesn’t really matter once you understand why you’re calling them.
What matters is how much control you have over that process.
Your API layer is your contract with the world outside your app.
Keep it clear. Keep it simple.
And if it makes your day easier, you’re already doing it right.
Top comments (0)