api-layer-title
api-layer-subtitle
api-layer-intro-note
🏗️ api-layer-what-title
Think of the API layer as a bouncer for your app — it decides who talks to the backend and how. No more spaghetti fetch calls scattered across your components! 🍝 Let's build a clean, organized gateway that keeps your UI and data layers happily separated.
🔵 Impact: MEDIUM — A dedicated API layer keeps your codebase organized and your components blissfully unaware of HTTP details
📋 In this section: API Layer Definition • Separation of Concerns • Benefits Overview
api-layer-what-benefit-title
api-layer-what-benefit-desc
🚨 api-layer-bad-title
We've all been there — fetch calls living right inside your components like uninvited guests at a party. 🎉 Let's see why mixing API logic with UI code is a recipe for spaghetti and how to fix it!
🔴 Impact: CRITICAL — Mixing API calls with UI code makes everything harder to test, debug, and maintain — don't do it!
📋 In this section: Anti-Pattern Examples • Mixed Concerns • Separation Solution
// ❌ BAD: Mixed API and UI codeimport { useEffect, useState } from "react";import { useParams } from "react-router";import { apiClient } from "@/api/client";export function UserProfile() {const { handle } = useParams<{ handle: string }>();const [user, setUser] = useState();const [userShouts, setUserShouts] = useState();const [hasError, setHasError] = useState(false);useEffect(() => {// API details mixed with UI logicapiClient.get(`/user/${handle}`) // Endpoint path in component.then((response) => setUser(response.data)).catch(() => setHasError(true));apiClient.get(`/user/${handle}/shouts`) // Another endpoint in component.then((response) => setUserShouts(response.data)).catch(() => setHasError(true));}, [handle]);if (hasError) return <div>An error occurred</div>;if (!user || !userShouts) return <LoadingSpinner />;return <div>{/* UI code */}</div>;}// Problems:// - Component knows about API endpoints (/user/:handle)// - Component knows about HTTP methods (GET)// - Component handles API response structure (response.data)// - Hard to reuse this API logic in other components// - If endpoint changes, must update component code
// ✅ GOOD: Separated API layer// src/api/user.tsimport { apiClient } from "./client";async function getUser(handle: string) {const response = await apiClient.get(`/user/${handle}`);return response.data;}async function getUserShouts(handle: string) {const response = await apiClient.get(`/user/${handle}/shouts`);return response.data;}export default { getUser, getUserShouts };// Component - clean and focused on UIimport UserApi from "@/api/user";export function UserProfile() {const { handle } = useParams<{ handle: string }>();const [user, setUser] = useState();const [userShouts, setUserShouts] = useState();const [hasError, setHasError] = useState(false);useEffect(() => {if (!handle) return;UserApi.getUser(handle).then((data) => setUser(data)).catch(() => setHasError(true));UserApi.getUserShouts(handle).then((data) => setUserShouts(data)).catch(() => setHasError(true));}, [handle]);// Component only cares about UIif (hasError) return <div>An error occurred</div>;if (!user || !userShouts) return <LoadingSpinner />;return <div>{/* UI code */}</div>;}// Benefits:// - Component doesn't know endpoint paths// - Component doesn't know HTTP methods// - API logic is reusable// - Easy to change API without touching UI
💡 api-layer-why-title
Why should you care about an API layer? Because your future self will thank you! 🙏 A proper API layer makes your app easier to maintain, test, and scale. Here are the key reasons it's a game-changer.
🟠 Impact: HIGH — Understanding WHY matters is the difference between a junior dev and a senior architect
📋 In this section: Separation of Concerns • Maintainability • Reusability • Flexibility
1. api-layer-why-separation-title
api-layer-why-separation-desc
2. api-layer-why-maintainability-title
api-layer-why-maintainability-desc
3. api-layer-why-reusability-title
api-layer-why-reusability-desc
4. api-layer-why-flexibility-title
api-layer-why-flexibility-desc
🪝 api-layer-hooks-title
Custom hooks are like superpowers for your API layer! 🦸 They encapsulate all the loading, error, and data management so your components stay clean and focused. Build once, use everywhere — that's the dream!
🟠 Impact: HIGH — Custom hooks turn repetitive fetch-and-state patterns into elegant, reusable one-liners
📋 In this section: useFetch Hook • Type-Safe Wrappers • Reusable API Hooks
// ❌ BAD: Basic fetch functions without hooks// api/user.tsexport async function getUser(id: string) {const response = await fetch(`/api/users/${id}`);return response.json();}// Component - must manage state manuallyfunction UserProfile({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);useEffect(() => {setLoading(true);getUser(userId).then(data => {setUser(data);setLoading(false);}).catch(err => {setError(err);setLoading(false);});}, [userId]);if (loading) return <div>Loading...</div>;if (error) return <div>Error occurred</div>;return <div>{user?.name}</div>;}// Problems:// - Repetitive state management in every component// - Loading and error states duplicated// - Not type-safe// - Can't easily cancel requests
// ✅ GOOD: Custom hooks for API layer// hooks/useFetch.tsimport { useState } from "react";type UseFetchProps = {url: string;method: "GET" | "POST" | "PUT" | "DELETE";};export function useFetch<T>({ url, method }: UseFetchProps) {const [isLoading, setIsLoading] = useState(false);const [data, setData] = useState<T | null>(null);const [error, setError] = useState<Error | null>(null);const commonFetch = async (input?: Record<string, any>) => {setIsLoading(true);setError(null);try {const response = await fetch(url, {method,headers: { "Content-Type": "application/json" },body: input ? JSON.stringify(input) : undefined,});if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const jsonData = await response.json();setData(jsonData);return jsonData;} catch (err) {const error = err instanceof Error ? err : new Error("Unknown error");setError(error);throw error;} finally {setIsLoading(false);}};return { isLoading, data, error, commonFetch };}// api/user.tsimport { useFetch } from "@/hooks/useFetch";import { User } from "@/types";export const useGetUser = () => {const { commonFetch, isLoading, data, error } = useFetch<User>({url: "/api/users",method: "GET",});const getUser = (id: string) =>commonFetch({ id });return { getUser, isLoading, data, error };};// Component - clean and declarativefunction UserProfile({ userId }) {const { getUser, isLoading, data: user, error } = useGetUser();useEffect(() => {getUser(userId);}, [userId]);if (isLoading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;return <div>{user?.name}</div>;}// Benefits:// - Reusable loading/error state// - Type-safe with TypeScript generics// - Consistent interface across all API calls// - Easy to extend with abort logic, caching, etc.
🧩 api-layer-complete-title
Time to put it all together! 🎯 This is the full blueprint for a production-grade API layer — types, requests, and aggregated hooks all working in harmony. Copy this structure and never look back!
🔴 Impact: CRITICAL — This is the full pattern you'll use in every production project — master it and level up your architecture game
📋 In this section: Folder Structure • TypeScript Types • Request Hooks • Hook Aggregation
api-layer-complete-structure-title
api/
supplier/
types.ts # TypeScript types
requests.ts # Individual API hooks
api.ts # Aggregated API hook
user/
types.ts
requests.ts
api.ts
hooks/
useFetch.ts # Base fetch hookapi-layer-complete-types-title
// ❌ Types scattered or missingfunction getUser(id) { // No type informationreturn fetch(`/api/users/${id}`);}// Component doesn't know what to expectconst user = await getUser("123");console.log(user.name); // TypeScript error: Property 'name' does not exist
// ✅ Types defined clearly// api/user/types.tsexport type User = {id: string;name: string;email: string;createdOn: string;};export type GetUserInput = { id: string };// api/user/requests.tsimport { useFetch } from "@/hooks/useFetch";import { User, GetUserInput } from "./types";export const useGetUser = () => {const { commonFetch, isLoading, data, error } = useFetch<User>({url: "/api/users/get",method: "GET",});const getUser = (input: GetUserInput) =>commonFetch({ input });return { getUser, isLoading, data, error };};// Component - fully type-safeconst { getUser, data } = useGetUser();await getUser({ id: "123" });// TypeScript knows data is User | nullconsole.log(data?.name); // ✅ Type-safe!
api-layer-complete-aggregation-title
// ❌ Multiple hooks imported separatelyimport { useGetUser } from "@/api/user/requests";import { useGetUserPosts } from "@/api/user/requests";import { useUpdateUser } from "@/api/user/requests";function UserProfile() {const { getUser, isLoading: getUserLoading } = useGetUser();const { getPosts, isLoading: getPostsLoading } = useGetUserPosts();const { updateUser, isLoading: updateLoading } = useUpdateUser();// Messy and verbose...
// ✅ Single aggregated hook// api/user/api.tsimport { useGetUser, useGetUserPosts, useUpdateUser } from "./requests";export const useUserApi = () => {const getUserHook = useGetUser();const getPostsHook = useGetUserPosts();const updateUserHook = useUpdateUser();return {getUser: {query: getUserHook.getUser,isLoading: getUserHook.isLoading,data: getUserHook.data,},getPosts: {query: getPostsHook.getUserPosts,isLoading: getPostsHook.isLoading,data: getPostsHook.data,},updateUser: {mutation: updateUserHook.updateUser,isLoading: updateUserHook.isLoading,data: updateUserHook.data,},};};// Component - clean and organizedimport { useUserApi } from "@/api/user/api";function UserProfile() {const {getUser: { query: getUser, data: user, isLoading: userLoading },getPosts: { query: getPosts, data: posts, isLoading: postsLoading },updateUser: { mutation: updateUser },} = useUserApi();// Clean, predictable interface!
🎯 api-layer-takeaways-title
🟢 Impact: LOW — Quick recap to cement everything you've learned — bookmark this for your next project!
📋 In this section: Core Principles • Best Practices Summary • Next Steps
✓ api-layer-takeaway-1
✓ api-layer-takeaway-2
✓ api-layer-takeaway-3
✓ api-layer-takeaway-4
✓ api-layer-takeaway-5