Skip to main content
โšก Calmops

Frontend-Backend Integration

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

  1. 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);
      }, []);
    }
    
  2. Handle loading states:

    // โœ… Good: Show loading state
    if (loading) return <Spinner />;
    
    // โŒ Bad: No loading state
    return <UserList users={users} />;
    
  3. 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

Next Steps

Comments