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:
fetch('/api/users')or know exactly how the server is built. Instead, the UI calls a function from a separate layer, for example:
// api/users.ts
export async function getUsers() {
const res = await fetch('/api/users');
return res.json();
}And in the component:
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:
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:
// 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:
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:
// 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:
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.
// 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.
// 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:
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:
queryFn: () => Promise<Data>Isolating the API makes the code natural:
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 onfetch; - Clean Architecture - the outer layer (API) is isolated from the inner one (UI).
An architecture example
src/
├── api/
│ ├── client.ts ← shared fetch/axios setup
│ ├── users.ts ← API functions
│ └── posts.ts
├── features/
│ ├── users/
│ │ ├── hooks.ts ← useUserQuery(), useCreateUserMutation()
│ │ ├── components/
│ │ │ └── UserList.tsx
│ │ └── index.ts
├── ui/
│ ├── components/
│ └── layout/
└── app.tsxAll the server communication logic lives in api/
Components → hooks → API → server.
Summary
| Advantage | What it gives you |
|---|---|
| Separation of concerns | The UI handles rendering, the API handles data |
| Centralized logic | tokens, errors, retries in one place |
| Easy backend changes | the UI doesn't break when the API changes |
| Simple testing | you can mock the API |
| Reuse | the same calls from different places |
| Compatibility with React Query / SWR | pure 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 readyA concise answer to help you respond confidently on this topic during an interview.