Introduction: Connecting Your Frontend and Backend

Welcome to the first lesson of this course, where we will set up the API client for your project. In modern web applications, the frontend (what users see and interact with) often needs to communicate with the backend (where data and business logic live). In our case, the frontend is built with React, and the backend uses NestJS.

To make this communication possible, we use an API client. The API client is a set of functions that help your React app send requests to the backend and handle the responses. Setting up this client is the first step in allowing your app to fetch data, show updates, and interact with users in real time.

This lesson makes your React app capable of talking to the backend by creating a small, environment-aware API client and then verifying connectivity from HomePage. We will:

  1. Define environment helpers and base URL candidates (isBrowser, isLocalhost, PORT_HOST, cachedBase, BASE_CANDIDATES) and explain them in depth.
  2. Centralize endpoint paths in a paths helper.
  3. Implement a tiny fetch-based apiClient and a hello() function that calls /api/hello.
  4. Use useEffect in HomePage to test the API status and display a user-friendly indicator.

By the end, you’ll understand exactly how URLs are chosen, why we cache the base, how the /api/hello request works, and how HomePage safely updates state after an async call.

Breaking Down the API Client Code

Let’s look at the main parts of the API client, which lives in src/api/client.ts. Here is the code:

// src/api/client.ts

export const paths = {
  hello: () => `/api/hello`,
};

export const hello = async () => {
  try {
    const res = await apiClient.get(paths.hello());
    console.log('API /api/hello response:', res);
    return res;
  } catch (err) {
    console.error('Error calling /api/hello:', err);
    throw err;
  }
};

export const apiClient = {
  get: <T = any>(path: string) => request<T>(path),
};

Let’s break this down:

  • Centralized endpoint paths: paths.hello() returns the canonical string "/api/hello". Keeping paths in one object avoids typos and makes future refactors (e.g., prefix changes) a single-edit operation.

  • Hello call flow: hello() performs apiClient.get(paths.hello()), which sends a GET request to /api/hello. It logs successes for dev visibility, throws on errors to let the caller decide how to handle failures, and returns the parsed JSON (the backend “envelope”).

  • Expected envelope: For this endpoint, the backend replies with { success: true, data: "Hello World!" }. Your UI reads success (for status) and may read data (the string) for confirmation.

  • hello is an asynchronous function. It uses await to wait for the API response.

  • It calls apiClient.get(paths.hello()), which sends a GET request to /api/hello.

  • If the request is successful, it logs the response and returns it.

  • If there is an error (for example, if the server is down), it logs the error and throws it so the calling code can handle it.

Example output in the browser console:

API /api/hello response: { success: true, message: "Hello from the backend!" }

or, if there is an error:

Error calling /api/hello: Error: Network Error

This function is a simple example of how your frontend can talk to your backend using the API client.

Using the API Client on the Home Page

Now, let’s see how the API client is used in the React frontend. Here is the relevant part of src/pages/HomePage.tsx:

import { useEffect, useState } from 'react';
import { hello } from '../api/client';

export default function HomePage() {
  const [serverStatus, setServerStatus] = useState('Checking...');
  
  useEffect(() => {
    let isMounted = true;

    hello()
      .then((res: any) => {
        if (isMounted) {
          console.log("res: ", res)
          if (res && res.data.success === true) {
            setServerStatus('Connected');
          } else {
            setServerStatus('Connection Failed');
          }
        }
      })
      .catch(() => {
        if (isMounted) setServerStatus('Connection Failed');
      });

    return () => {
      isMounted = false;
    };
  }, []);

  return (
    <section className="text-center">
      <h1 className="text-4xl font-bold">Welcome to ShelfPilot</h1>
      <p className="mt-4 text-lg text-slate-300">
        Your journey to organized reading starts here.
      </p>
      <p className="mt-8 text-sm text-slate-500">
        API Status:{' '}
        <span className={serverStatus === 'Connected' ? 'text-green-400' : 'text-red-400'}>
          {serverStatus}
        </span>
      </p>
    </section>
  );
}

Here’s what’s happening:

  • The HomePage component uses React’s useState and useEffect hooks.
  • When the page loads, it calls the hello function from the API client.
  • If the backend responds with { success: true }, it sets the status to "Connected."
  • If there is an error or the response is not successful, it sets the status to "Connection Failed."
  • The status is displayed on the page, so users can see if the frontend is able to talk to the backend.

Example output on the page:

API Status: Connected

or

API Status: Connection Failed

This is a simple but powerful way to check if your frontend and backend are connected.

HomePage useEffect

Let's break down the usage of the useEffect hook we saw in the HomePage.tsx component.

  • State Setters (setServerStatus, setHelloMessage)

    • useState returns a getter and a setter. Calling a setter schedules a re-render with the new value.
    • We initialize serverStatus to "Checking..." so the UI communicates that a health check is in progress.
    • When a response arrives, we flip it to "Connected" or "Connection Failed". If we receive a greeting string in data, we store it in helloMessage.
  • The Effect Lifecycle

    • The effect runs once after the first render because the dependency array is [].
    • Inside the effect, we call hello() which returns a promise resolving to ApiEnvelope<string>.
    • When the promise resolves successfully, we extract success and data from the envelope. This mirrors the backend’s global response contract.
    • If success is true, the server is reachable and responded as expected. If false, we treat it as a connectivity or health failure for the purpose of this check.
  • isMounted Guard

    • let isMounted = true; is a safety flag to avoid setting state after the component unmounts.
    • Why needed: async promises may resolve after a route change or rapid navigation. Updating state on an unmounted component leads to React warnings and is wasted work.
    • We check if (!isMounted) return; inside both .then and .catch.
    • The cleanup function return () => { isMounted = false; } runs automatically on unmount (and before the effect re-runs, if it had dependencies). This makes late promise resolutions no-ops.
  • The catch Block

    • Catches network errors, CORS issues, JSON parsing errors, or any thrown exceptions in the chain.
    • We still guard with isMounted and then set the status to "Connection Failed" to show a clear user-facing signal.
    • Optional logging (commented) can help diagnose issues during development without spamming the console in production.
  • Extracting Success from the Backend

    • We treat the backend response as ApiEnvelope<HelloData>. The fields of interest are success and data.
    • Reading success standardizes how we interpret results across endpoints. For /api/hello, a healthy response is { success: true, data: "Hello World!" }.
    • Storing the data string in helloMessage is optional but helpful; it confirms that not only did the request succeed, but the payload is exactly what we expected.
Why This Structure Works
  • Environment helpers make URLs portable across local dev, sandboxed ports, and same-origin reverse proxies.
  • A shared paths object removes stringly-typed endpoints and helps avoid typos.
  • Returning the envelope from hello() keeps the calling UI consistent with other endpoints that will be added later.
  • The useEffect pattern with an isMounted guard is robust for small health checks and scales to more complex data fetching where cancellation or staleness matters.

With this foundation, your frontend speaks reliably to the backend and communicates status clearly to users and developers. Next, you can build additional client helpers (auth, books, shelf) using the same envelope pattern and paths registry.

Deep Dive: Breaking Down the API Client Code
Deep Dive: The request<T>() pipeline (step-by-step)
Summary and What’s Next

In this lesson, you learned how to set up an API client to connect your React frontend to your NestJS backend. We covered:

  • Why an API client is important
  • How the client code detects the environment and chooses the right base URL
  • How to make a simple API call to the backend
  • How to use the API client in a React component to show the connection status

You are now ready to practice making and using API calls in your own code. In the next exercises, you will get hands-on experience with these concepts, helping you build confidence in connecting your frontend and backend.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal