Frontend-Backend Integration
Frontend-backend integration is crucial for building cohesive applications. This article covers integration patterns and best practices.
Introduction
Frontend-backend integration provides:
- Seamless communication
- Data consistency
- Error handling
- Performance optimization
- User experience
Understanding integration helps you:
- Communicate with APIs
- Manage state
- Handle errors
- Optimize performance
- Build cohesive applications
API Communication
Fetch API
// โ
Good: Basic fetch
async function getUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
// โ
Good: POST request
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
// โ
Good: Request with authentication
async function getProtectedData() {
const token = localStorage.getItem('token');
const response = await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Token expired, refresh it
await refreshToken();
return getProtectedData();
}
return response.json();
}
Axios
// โ
Good: Install Axios
// npm install axios
import axios from 'axios';
// โ
Good: Create Axios instance
const api = axios.create({
baseURL: 'http://localhost:3000/api',
timeout: 10000
});
// โ
Good: Request interceptor
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// โ
Good: Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Refresh token
const newToken = await refreshToken();
localStorage.setItem('token', newToken);
// Retry request
return api.request(error.config);
}
return Promise.reject(error);
}
);
// โ
Good: API calls
async function getUsers() {
const response = await api.get('/users');
return response.data;
}
async function createUser(userData) {
const response = await api.post('/users', userData);
return response.data;
}
State Management Integration
React with API
import { useState, useEffect } from 'react';
// โ
Good: Fetch data on mount
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// โ
Good: Custom hook for API calls
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function App() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return <div>{users?.length} users</div>;
}
Redux with API
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// โ
Good: Async thunk for API calls
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// โ
Good: Slice with async thunk
const usersSlice = createSlice({
name: 'users',
initialState: {
data: [],
loading: false,
error: null
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export default usersSlice.reducer;
// Usage
function UserList() {
const dispatch = useDispatch();
const { data: users, loading, error } = useSelector(state => state.users);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Error Handling
Error Handling Strategy
// โ
Good: Centralized error handling
class APIError extends Error {
constructor(message, status, data) {
super(message);
this.status = status;
this.data = data;
}
}
async function handleAPICall(fn) {
try {
return await fn();
} catch (error) {
if (error instanceof APIError) {
// Handle API error
console.error(`API Error ${error.status}: ${error.message}`);
if (error.status === 401) {
// Redirect to login
window.location.href = '/login';
} else if (error.status === 403) {
// Show permission error
showError('You do not have permission');
} else if (error.status === 500) {
// Show server error
showError('Server error occurred');
}
} else {
// Handle network error
console.error('Network error:', error);
showError('Network error occurred');
}
throw error;
}
}
// โ
Good: Error boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong</h1>;
}
return this.props.children;
}
}
Performance Optimization
Request Optimization
// โ
Good: Debounce search requests
function useSearch(query) {
const [results, setResults] = useState([]);
useEffect(() => {
const timer = setTimeout(async () => {
if (query) {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
}
}, 300);
return () => clearTimeout(timer);
}, [query]);
return results;
}
// โ
Good: Cache API responses
const cache = new Map();
async function getCachedData(url) {
if (cache.has(url)) {
return cache.get(url);
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, data);
return data;
}
// โ
Good: Pagination
async function getUsers(page = 1, limit = 10) {
const response = await fetch(`/api/users?page=${page}&limit=${limit}`);
return response.json();
}
Response Optimization
// โ
Good: Lazy load data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
// Load user immediately
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
// Load posts later
setTimeout(() => {
fetch(`/api/users/${userId}/posts`)
.then(r => r.json())
.then(setPosts);
}, 1000);
}, [userId]);
return (
<div>
{user && <h1>{user.name}</h1>}
{posts && <PostList posts={posts} />}
</div>
);
}
// โ
Good: Partial data loading
async function getUser(userId, fields = ['id', 'name', 'email']) {
const response = await fetch(
`/api/users/${userId}?fields=${fields.join(',')}`
);
return response.json();
}
Best Practices
-
Separate API logic:
// โ Good: Separate API service // services/userService.js export const getUsers = () => fetch('/api/users').then(r => r.json()); export const createUser = (data) => fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }); // components/UserList.js import { getUsers } from '../services/userService'; // โ Bad: API logic in component function UserList() { useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []); } -
Handle loading states:
// โ Good: Show loading state if (loading) return <Spinner />; // โ Bad: No loading state return <UserList users={users} />; -
Validate data:
// โ Good: Validate API response const response = await fetch('/api/users'); const data = response.json(); if (!Array.isArray(data)) { throw new Error('Invalid response'); } // โ Bad: No validation const data = await response.json();
Summary
Frontend-backend integration is essential. Key takeaways:
- Use appropriate HTTP clients
- Manage state effectively
- Handle errors properly
- Optimize requests
- Separate concerns
- Validate data
- Show loading states
- Cache responses
Related Resources
Next Steps
- Learn about Deployment
- Explore Monitoring
- Study Performance Testing
- Practice integration
- Build integrated applications
Comments