Skip to main content

Isolating API from UI

What "isolating the API layer from the UI" means

Isolating the API layer means separating the code that:

  • talks to the server (HTTP requests, REST, GraphQL, etc.), from the code that:
  • renders the interface (React components, state, events).

That is, the UI should not directly call:

javascript
fetch('/api/users')

or know exactly how the server is built. Instead, the UI calls a function from a separate layer, for example:

javascript
// api/users.ts export async function getUsers() { const res = await fetch('/api/users'); return res.json(); }

And in the component:

javascript
const { data } = useQuery({ queryKey: ['users'], queryFn: getUsers });

The main idea

The UI should know only what needs to be fetched, but not exactly how to fetch it.

This is an implementation of the Separation of Concerns principle.


Why this matters

1. Clean architecture and readability

When API logic (requests, headers, authorization) is mixed with UI code, components become "dirty" and hard to read:

javascript
function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users', { headers: { Authorization: token } }) .then(res => res.json()) .then(setUsers); }, []); // UI + networking details mixed together return users.map(u => <div>{u.name}</div>); }

After isolation:

javascript
// api/users.ts export const getUsers = () => api.get<User[]>('/users'); // a centralized fetch wrapper // components/UserList.tsx const { data: users } = useQuery({ queryKey: ['users'], queryFn: getUsers });

The component is responsible only for rendering, and the API code is responsible only for requests.


2. Reusability

If several places need to fetch the same user:

javascript
useQuery({ queryKey: ['user', id], queryFn: () => getUser(id) });

you use the same API function, instead of copying fetch into every component.


3. Testability

When the API is isolated, you can:

  • mock it in tests;
  • test the UI without a real server.

Example:

javascript
// jest.setup.ts jest.mock('@/api/users', () => ({ getUsers: jest.fn(() => Promise.resolve([{ id: 1, name: 'Alex' }])), }));

Tests for the component now don't depend on the network:

javascript
render(<UserList />); expect(screen.getByText('Alex')).toBeInTheDocument();

4. Centralized error handling and authorization

When all requests go through a single point (apiClient), you can:

  • automatically attach a token,
  • intercept 401 and refresh the JWT,
  • log errors,
  • handle timeouts.
javascript
// api/client.ts export const api = axios.create({ baseURL: '/api', }); api.interceptors.response.use( res => res, err => { if (err.response?.status === 401) refreshToken(); return Promise.reject(err); } );

Now you don't need to repeat this code in every component.


5. Easy backend replacement

If the API changes (for example, REST → GraphQL, or a new endpoint), only the API layer changes, and the UI code isn't touched at all.

javascript
// before export const getUser = (id: string) => fetch(`/api/users/${id}`).then(r => r.json()); // after export const getUser = (id: string) => graphQLClient.request(GET_USER_QUERY, { id });

The components stay the same:

javascript
const { data } = useQuery({ queryKey: ['user', id], queryFn: () => getUser(id) });

6. Integration with React Query / SWR / Zustand

Modern data managers (React Query, SWR) expect a plain function:

javascript
queryFn: () => Promise<Data>

Isolating the API makes the code natural:

javascript
useQuery({ queryKey: ['products'], queryFn: getProducts });

→ React Query caches, updates, and synchronizes the data itself.


7. Alignment with architectural principles

This matches the principles of:

  • SRP (Single Responsibility Principle) - each module does one thing;
  • Separation of Concerns - the UI doesn't know about network details;
  • Dependency Inversion - React depends on an abstraction (getUser), not on fetch;
  • Clean Architecture - the outer layer (API) is isolated from the inner one (UI).

An architecture example

javascript
src/ ├── api/ │ ├── client.ts ← shared fetch/axios setup │ ├── users.tsAPI functions │ └── posts.ts ├── features/ │ ├── users/ │ │ ├── hooks.tsuseUserQuery(), useCreateUserMutation() │ │ ├── components/ │ │ │ └── UserList.tsx │ │ └── index.ts ├── ui/ │ ├── components/ │ └── layout/ └── app.tsx

All the server communication logic lives in api/ Components → hooks → API → server.


Summary

AdvantageWhat it gives you
Separation of concernsThe UI handles rendering, the API handles data
Centralized logictokens, errors, retries in one place
Easy backend changesthe UI doesn't break when the API changes
Simple testingyou can mock the API
Reusethe same calls from different places
Compatibility with React Query / SWRpure functions with no side effects

In short:

Isolating the API layer from the UI makes the application cleaner, more reliable, and easier to maintain.

React components should think only about what to display, not how to fetch the data.

Everything network-related, authorization-related, and technical is moved into the API layer.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.