Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iDevRanjan/lws-ra-b4-assignment-five/llms.txt

Use this file to discover all available pages before exploring further.

Every network request in LWS Job Portal flows through one of two Axios clients: a plain axios instance for unauthenticated public endpoints, and a configured axiosInstance that automatically attaches a JWT Bearer token to every request. On top of those clients, TanStack React Query provides caching, background refresh, loading/error states, and infinite pagination — all driven by reusable factory functions in queryOptions.js and mutationOptions.js so the same query configuration is never written twice.

Axios Setup

The authenticated instance

src/services/axiosInstance.js creates an Axios instance with a base URL and two interceptors. The request interceptor reads the current auth token from the pub-sub store on every call — no need to restart the app or manually refresh headers after login.
// src/services/axiosInstance.js
export const axiosInstance = axios.create({
    baseURL: "http://localhost:9000",
});

axiosInstance.interceptors.request.use(
    (config) => {
        const authData = store.getState().authData;

        if (authData?.token) {
            config.headers.Authorization = `Bearer ${authData.token}`;
        }

        return config;
    },
    (error) => Promise.reject(error),
);

axiosInstance.interceptors.response.use(
    (response) => response,
    (error) => {
        if (
            error.response &&
            (error?.response.status === 401 || error?.response.status === 403)
        ) {
            authActions.logout();
        }

        return Promise.reject(error);
    },
);
The response interceptor calls authActions.logout() on any 401 or 403. This clears the query cache, removes the token from localStorage, and resets the store atomically — ensuring a seamlessly de-authenticated UI without any component needing to handle auth errors manually.
The axiosInstance base URL is hardcoded to http://localhost:9000 and should be updated to match your deployment target. Public API modules (jobApi.js, authApi.js, companyApi.js) construct their full URLs from the Vite environment variable import.meta.env.VITE_API_BASE_URL and use a plain axios instance. Authenticated endpoints use axiosInstance directly with path-relative URLs (e.g., "/api/users/profile").

API Modules

jobApi.js

Public job data. Uses plain axios for getAllJobs, getJobBySlug, getSimilarJobs (no auth needed) and axiosInstance for getRecommendedJobs (requires login).

authApi.js

applicationLogin and clientRegister — both plain axios.post calls with no prior auth token.

companyApi.js

Mixed: public getCompanyBySlug / getCompanyOpenPositions via plain axios; authenticated company management endpoints via axiosInstance.

userApi.js

All authenticated: profile read/update, resume upload, avatar upload, job applications. Uses axiosInstance exclusively.

jobApi.js

// src/services/jobApi.js
export async function getAllJobs(pageParam, params) {
    const response = await axios.get(
        `${BASE_URL}/api/jobs?page=${pageParam}${params ? `&${params}` : ""}`,
    );
    return response.data;
}

export async function getJobBySlug(jobSlug) {
    const response = await axios.get(`${BASE_URL}/api/jobs/${jobSlug}`);
    return response.data;
}

export async function getSimilarJobs(jobId) {
    const response = await axios.get(`${BASE_URL}/api/jobs/${jobId}/similar`);
    return response.data;
}

export async function getRecommendedJobs() {
    const response = await axiosInstance.get("/api/jobs/recommendations");
    return response.data;
}

authApi.js

// src/services/authApi.js
export async function applicationLogin(loginFormData) {
    const response = await axios.post(`${BASE_URL}/api/auth/login`, loginFormData);
    return response.data;
}

export async function clientRegister(registerFormData) {
    const response = await axios.post(`${BASE_URL}/api/auth/register`, registerFormData);
    return response.data;
}

userApi.js

// src/services/userApi.js
export async function getUserProfile() {
    const response = await axiosInstance.get("/api/users/profile");
    return response.data;
}

export async function updateJobSeekerProfile(profileFormData) {
    const response = await axiosInstance.put("/api/users/profile", profileFormData);
    return response.data;
}

export async function updateJobSeekerAvatar(avatarFormData) {
    const response = await axiosInstance.post("/api/users/profile-picture", avatarFormData);
    return response.data;
}

export async function updateJobSeekerResume(resumeFormData) {
    const response = await axiosInstance.post("/api/users/resume", resumeFormData);
    return response.data;
}

export async function applyAJob(coverLetterFormData, jobId) {
    const response = await axiosInstance.post(
        `/api/applications/jobs/${jobId}/apply`,
        coverLetterFormData,
    );
    return response.data;
}

export async function withdrawApplication(applicationId) {
    const response = await axiosInstance.delete(`/api/applications/${applicationId}`);
    return response.data;
}

TanStack React Query Patterns

useQuery — single resource fetching

Single-resource queries are the most common pattern. They use queryOptions() factory functions so the same query key and fetch function work in both hooks and router loaders.
// useProfile.js — composed hook
export function useProfile() {
    const { authData } = useAuth();
    return useQuery(getClientProfileQueryOption(authData));
}

// getClientProfileQueryOption factory
export function getClientProfileQueryOption(authData) {
    return queryOptions({
        queryKey: [QUERY_KEYS.clientProfile, authData.role],
        queryFn: () => {
            if (authData.role === "USER") return getUserProfile();
            if (authData.role === "COMPANY") return getCompanyProfile();
            throw new Error("Invalid role");
        },
        enabled: authData.isLoggedin,
        retry: false,
    });
}
The enabled flag prevents the query from firing when the user is not logged in. retry: false ensures that a failed profile fetch (e.g., a 401) is not retried — the response interceptor already handles logout on 401.

useInfiniteQuery — paginated job listings

The home page job listing and the company applicants list both use useInfiniteQuery for load-more infinite scroll. The pageParam is an integer page number; getNextPageParam returns undefined to signal the end of results.
// getAllJobsQueryOption — supports useInfiniteQuery
export function getAllJobsQueryOption(params) {
    return queryOptions({
        queryKey: [QUERY_KEYS.allJobs, params],
        queryFn: ({ pageParam }) => getAllJobs(pageParam, params),
        initialPageParam: 1,
        placeholderData: keepPreviousData,
        getNextPageParam: (lastPage, allPages) => {
            if (lastPage.data.length === 0) {
                return undefined;
            }
            return allPages.length + 1;
        },
    });
}
1

Initial fetch

useInfiniteQuery calls queryFn with pageParam = 1 (from initialPageParam). The API returns the first page of jobs.
2

Load More triggered

The user clicks “Load More”. React Query calls queryFn again with pageParam = 2 (returned by getNextPageParam). New results are appended to data.pages.
3

Filter change resets

When params changes (new search or filter), the queryKey changes. React Query treats this as a brand-new query and restarts from initialPageParam: 1, ensuring users always see relevant results from the top.
4

End of results

When the API returns an empty data array, getNextPageParam returns undefined. React Query sets hasNextPage = false and the Load More button is hidden.
keepPreviousData (via placeholderData) means the current page list stays visible while the next page is loading — preventing a full loading flash on every filter change. The company applicants list uses the same shape:
export function getCompanyApplicantsQueryOption(params) {
    return queryOptions({
        queryKey: [QUERY_KEYS.companyApplicants, params],
        queryFn: ({ pageParam }) => getCompanyApplicants(pageParam, params),
        initialPageParam: 1,
        placeholderData: keepPreviousData,
        getNextPageParam: (lastPage, allPages) => {
            if (lastPage.data.length === 0) return undefined;
            return allPages.length + 1;
        },
    });
}

useMutation — creating and updating data

Mutations use mutationOptions() factory functions. Side-effects — particularly cache invalidation — live inside onSuccess callbacks so the UI stays consistent after a write.
// applyAJobMutationOption — apply and refresh application list
export function applyAJobMutationOption(jobId) {
    return mutationOptions({
        mutationFn: (coverLetterFormData) =>
            applyAJob(coverLetterFormData, jobId),
        onSuccess: async () => {
            await queryClient.invalidateQueries({
                queryKey: [QUERY_KEYS.jobSeekerApplications],
            });
        },
    });
}

// createCompanyJobMutationOption — post a new job listing
export function createCompanyJobMutationOption() {
    return mutationOptions({
        mutationFn: (payload) => createCompanyJob(payload),
    });
}

// applicationStatusUpdateMutationOption — accept/reject an applicant
export function applicationStatusUpdateMutationOption() {
    return mutationOptions({
        mutationFn: ({ applicationId, payload }) =>
            applicationStatusUpdate(applicationId, payload),
    });
}

Caching Strategy

// src/services/queryClient.js
export const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 1 * 60 * 1000,       // data is fresh for 1 minute
            gcTime: 2.5 * 60 * 1000,         // unused cache lives for 2.5 minutes
            refetchOnMount: false,
            refetchOnReconnect: false,
            refetchOnWindowFocus: false,
        },
    },
});
SettingValueEffect
staleTime60 000 msCached data is returned instantly without a background refetch for 1 minute
gcTime150 000 msInactive cache entries are garbage-collected after 2.5 minutes
refetchOnMountfalseRe-visiting a page does not trigger a refetch if data is still fresh
refetchOnReconnectfalseComing back online does not trigger automatic refetches
refetchOnWindowFocusfalseSwitching browser tabs and back does not trigger refetches
These conservative settings are intentional for a job portal: job listings don’t change second-to-second, and unnecessary refetches on tab-focus would waste bandwidth. Manual invalidation (queryClient.invalidateQueries) is used instead wherever fresh data is specifically required after a write.

Loading States: Skeleton Components

Every major data list has a corresponding skeleton component rendered while the query isLoading. This gives users an immediate structural preview of the page without a blank flash.
Skeleton componentUsed during
JobCardSkeletonHome page infinite job listing
RecentJobsCardSkeletonRecent jobs sidebar
ApplicationJobCardSkeletonJob seeker applications list
ApplicantsCardSkeletonCompany applicants list
ManageJobsTableRowSkeletonCompany manage-jobs table
SimilarJobSkeletonSimilar jobs panel on job detail page
A typical pattern in a list component:
if (isLoading) {
    return Array.from({ length: 5 }).map((_, i) => (
        <JobCardSkeleton key={i} />
    ));
}

Error Handling

Errors surface through two complementary channels:
When a loader throws (e.g., getJobBySlugLoader gets a 404), React Router activates <ErrorElement> inside MainLayout — the user sees a friendly error page with the header and footer still visible. No component-level error handling is needed for loader errors.
React Query DevTools are included in the project but commented out in src/App.jsx. To enable them during local development, uncomment the import and JSX tag:
// src/App.jsx
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// Inside the return:
<ReactQueryDevtools initialIsOpen={false} />
DevTools provide a live view of every query key, its status, stale time, cache data, and refetch history — invaluable when debugging unexpected loading states or cache misses.

Build docs developers (and LLMs) love