Welcome to the first lesson of this course on building a modern React frontend with a NestJS API. In this lesson, you will learn how to fetch and display a user's personalized reading shelf. The "shelf" is a feature that lets users keep track of books they are reading, want to read, or have finished. By the end of this lesson, you will understand how to retrieve this shelf data from the backend and show it in the frontend, setting the stage for more advanced features later.
To get oriented, let’s look at the main files used in this feature. These are the building blocks that combine backend API calls, type safety, and UI components into a single working page. Each file has a distinct responsibility, which helps keep our project modular and easy to extend. Here is the structure relevant to this unit:
src/lib/types.tsprovides strong type definitions for shelf data, ensuring consistency with backend responses.src/api/reading.tscentralizes API calls, so our UI never has to deal with raw HTTP logic.src/features/shelf/MyShelfPage.tsxis the main React page where the shelf is rendered.src/features/shelf/ShelfFilters.tsxcontains the dropdowns and buttons that let users change filters.src/components/Skeleton.tsxprovides a placeholder while data loads.src/App.tsxmanages navigation and adds prefetching so shelf data feels instant.
Together, these files provide everything we need to build a polished and responsive shelf page.
Before we start coding, it is important to understand how the backend serves shelf data. The backend provides a protected endpoint that responds only when a valid authentication token is supplied, ensuring users can only access their own data. This endpoint supports query parameters for filtering and sorting, allowing us to provide a dynamic and user-friendly shelf view. Understanding this route is critical because our frontend logic will closely mirror these query options.
- GET /reading/shelf (Protected: requires authentication)
- Purpose: Returns the current user’s shelf entries, enriched with book details such as title, author, and progress.
- Query params (all optional):
status: one of'not-started' | 'in-progress' | 'completed' | 'want-to-read'sortBy: one of'title' | 'author' | 'updatedAt' | 'progress'order:'asc' | 'desc'
- Response shape:
Note: You can always test the backend routes on your own. During the practices, open a new terminal and send a curl request to the desired route to check the output. For protected routes, you’ll need to log in with the correct credentials and include the access token in your request headers. For example, you can log in as admin and capture the access token like this:
We now define strong TypeScript types to match the backend response. These types not only give us autocomplete in the editor but also prevent runtime errors by enforcing valid values. With them, our frontend code will always stay in sync with backend expectations.
ShelfStatusdefines the valid reading states a book can have, directly matching backend validation.ShelfItemDtocaptures the full shape of a shelf entry, including progress and timestamps.GetShelfParamsspecifies optional filters and sorting, keeping the API call flexible.
By adding these types, we give our components strong guarantees about what kind of data they are working with, reducing bugs and making the code easier to maintain.
Once we have defined types, we can create a reusable function that fetches shelf data from the backend. This function uses our centralized apiClient, which is already configured with authentication headers, so we don’t need to manually attach tokens in each request. By dynamically building query strings, it allows us to fetch exactly the data the user wants to see.
To get the user's shelf, we use a function called getShelf in reading.ts. This function calls the backend API and returns a list of books on the user's shelf. It can also filter and sort the results based on the parameters you provide.
Here is the code for getShelf:
Explanation:
- The
paramsargument is optional but can includestatus,sortBy, andorder. URLSearchParamsdynamically builds a query string only with the provided keys.- The
apiClienthandles authentication headers, so each call automatically includes the user’s token.
To let users filter and sort their shelf view, we introduce a dedicated filter component. This component includes dropdowns for filtering by reading status and sorting by different fields, as well as a button to toggle the sort order. By encapsulating this logic in its own component, we keep the main page clean and make it easier to maintain or enhance filters later.
- The status dropdown lets users filter by reading phase (in-progress, completed, etc.).
- The sorting dropdown offers different sorting criteria like title, author, or progress.
- The button toggles between ascending and descending order.
- Each change triggers
onChange, which updates the parent’s state and re-queries the backend.
This UI layer is simple but powerful, allowing end users to interact with the backend via query parameters seamlessly.
Fetching data can take time, and leaving the UI blank during loading is a poor experience. To fix this, we add a skeleton loader that shows placeholder bars where content will appear. This gives users a sense that content is on the way and keeps the interface responsive.
- Renders a configurable number of placeholder bars (
linesdefaults to 3). - Applies accessibility attributes like
role="status"andaria-live="polite"to announce loading states. - Uses consistent background and sizing to match the design language of the app.
By using skeletons, we improve perceived performance and reduce the frustration of waiting for network calls.
Now, let’s see how the frontend fetches and displays this shelf data. The main logic is in MyShelfPage.tsx, which uses React Query to manage data fetching and state.
Here is the key part of the code:
Explanation:
- We call
useSearchParamsto synchronize filters with the URL, making the shelf view shareable and bookmarkable. - The
useQueryhook extractsdata,isLoading, andisErrorfrom the API call. - The
queryKeyuniquely identifies the query in React Query’s cache, ensuring data is refetched when filters change. queryFncalls ourgetShelfhelper, passing in the currentstatus,sortBy, and .
The shelf page includes filter controls so users can view books by status, sort order, or other criteria. These controls are in the ShelfFilters.tsx component.
Here’s how the filters work:
- The
status,sortBy, andordervalues come from the URL search parameters. - When a user changes a filter, the
onChangefunction updates the URL, which triggers React Query to refetch the shelf data with the new filters.
Example:
- If a user selects "Completed" from the status dropdown, the URL updates to include
?status=completed. - The shelf list updates to show only completed books.
This approach keeps the UI and the URL in sync, so users can bookmark or share filtered views of their shelf.
To make navigation smoother, we add prefetching so shelf data loads before the user even clicks. This makes the shelf feel instant when accessed from the navigation bar. The logic lives inside App.tsx and leverages React Query’s prefetchQuery.
onMouseEntertriggers prefetch before the user clicks.- React Query stores the data in cache so it is instantly available when navigating.
- Uses the same
getShelffunction for consistency.
This small optimization significantly improves perceived performance and responsiveness.
In React Query, a query represents a read-only request for data. Unlike mutations (which change data), queries are all about fetching and caching information from the server.
In this project, fetching the shelf is the most common example of a query.
A query is any operation that retrieves data.
Examples in this app:
- Fetching the user’s shelf (
getShelf) - Loading catalog books
- Getting user profile info
👉 Compare queries with mutations:
useQuery is designed to handle data fetching and caching with almost no boilerplate. It takes care of:
- Running your API call (
queryFn) - Caching the result (
queryKey) - Tracking loading and error states (
isLoading,isError) - Refetching automatically when dependencies change
This means you don’t have to write useEffect + useState + manual fetch logic. Everything is declarative.
Here’s the basic structure you’ll see in this project:
-
queryKey→ A unique identifier for the query.- Used for caching: if the same key is requested again, React Query reuses the data.
- Can include parameters so filtered results don’t clash.
-
queryFn→ The function that actually fetches the data (here,getShelf). -
data→ The response from your query (shelf items). -
isLoading→truewhile the request is in flight. -
isError→trueif the request fails. -
keepPreviousData→ Keeps the old result visible until the new one arrives, preventing flicker.
-
Initial load
isLoading = true- No data yet → show a skeleton loader.
-
Success
datais filled with results.- UI renders the shelf.
-
Error
isError = true- UI shows a fallback message.
-
Refetch
- Happens automatically when
queryKeychanges (e.g., user selects a different filter). - Can also be triggered manually with
invalidateQueries.
- Happens automatically when
In MyShelfPage.tsx, useQuery fetches the shelf like this:
Here’s what happens:
- The component mounts.
- React Query runs
getShelfwith the current filters. - While waiting, the page shows a skeleton loader.
- Once the data arrives, the shelf list renders immediately.
- If the user changes filters, the
queryKeychanges → React Query automatically refetches with new params.
Prefetching Queries
Another advantage of queries is that you can prefetch them.
In App.tsx, when a user hovers over the "My Shelf" link, the app prefetches the shelf query. This makes the page load instantly when clicked.
🔑 Key Takeaways
- Use
useQueryfor read-only data fetching. queryKeyuniquely identifies cached data.- React Query handles loading, error, and caching states automatically.
- Use
keepPreviousDatafor smoother transitions between filters. - Queries can be prefetched to make navigation feel instant.
In this lesson, you learned how to fetch and display a user's personalized shelf using a React frontend and a NestJS API. You saw how the getShelf function works, how React Query manages data fetching and state, and how filter controls update the shelf view in real time.
Next, you will get hands-on practice with these concepts. You’ll try fetching shelf data, handling loading and error states, and connecting filters to the shelf view yourself. This will help you build confidence in working with real-world data and user interfaces.
