Introduction: Why Login and Protected Routes Matter

Welcome back! In the previous lesson, you learned how to set up an API client so your React app can talk to your NestJS backend. Now, let’s take the next step: making sure only logged-in users can access certain parts of your app.

Most modern web apps need to know who their users are. This is called authentication. For example, you might want anyone to see your catalog, but only logged-in users should see their personal shelf. To do this, you need a way for users to log in and a way to protect certain routes so only authenticated users can access them.

In this lesson, you will learn how to:

  • Build a login form that talks to your backend.
  • Store a user’s login token.
  • Protect routes so only logged-in users can visit them.

Let’s get started!

Quick Recap: Routing Setup

Before we dive into authentication, let’s quickly remind ourselves how routing is set up in your app. You already have a router that defines which component shows up for each URL. Here’s a simplified version of your router setup:

This setup lets users visit /, /catalog, /login, and /shelf. Right now, anyone can visit any page. In this lesson, you’ll learn how to make /shelf available only to logged-in users.

Building the Login Flow

Let’s start by building the login flow. This means creating a form where users can enter their username and password, sending that data to your backend, and saving the token you get back. Below we’ll turn a simple form into a working login flow that talks to /auth/login, stores the returned token, and redirects the user. We’ll also add a post method to apiClient, explain useNavigate, and detail how the three useState hooks, e.preventDefault(), and error handling work together.

Here’s the code for your login form:

How the login form works (line by line)

Why TOKEN_KEY, and where do we store it?
  • TOKEN_KEY: A constant string used as the storage key. Keeping it centralized avoids typos and makes refactors (like namespacing) trivial.
  • localStorage: A browser key–value store that persists across tabs and page reloads for the same origin. Ideal for remembering login sessions after refresh.
  • Security note: Any token stored in web storage is vulnerable to XSS. Keep your app free of injection risks. If you later use HTTP-only cookies, you trade off different concerns (CSRF).
  • sessionStorage (FYI): Same API, but cleared when the tab closes. Prefer it if you want logins to expire with the tab/session rather than persist.

This code uses the browser’s localStorage to keep the token even if the user refreshes the page.

What does the user see?

  • If the login is successful, they are taken to /shelf.
  • If the login fails, they see:
    Invalid username or password.
Adding post to apiClient

Why add post here?

  • Single source of truth for transport: apiUtils centralizes how requests are made (headers, base URL selection, error parsing). Adding post here means every feature can use apiClient.post(...) without re-implementing fetch details.
  • Automatic JSON handling: The request<T>() helper adds Content-Type: application/json when a body exists and parses JSON (or returns text) uniformly, so your feature code stays clean.
  • Consistent behavior: Timeouts, auth headers, or retry strategies (if you add them later) can be applied in one place.

With post available, you can now call apiClient.post('/auth/login', { username, password }) in the login form.

Protecting Routes with ProtectedRoute

Now, let’s make sure only logged-in users can visit the /shelf page. We do this by creating a special component called ProtectedRoute. Once we have a token, we must gate certain routes so only authenticated users can access them. ProtectedRoute reads the token and either renders children or redirects to /login.

Here’s the code:

How does this work?

  • Token check: Reads from localStorage via getToken(). If no token, the user is considered unauthenticated.
  • Redirect: <Navigate to="/login" replace /> sends the user to the login page and replaces history so they don’t “Back” into a protected URL.
  • Outlet: When authenticated, <Outlet /> renders the matched child route inside the current layout, preserving headers/footers.

Now we make /shelf a child of ProtectedRoute.

We should also update our router to use ProtectedRoute for the route:

Summary And What’s Next

In this lesson, you learned how to:

  • Build a login form that talks to your backend and saves a token.
  • Use that token to protect certain routes in your app.
  • Redirect users to the login page if they are not authenticated.

These are the building blocks for user authentication in your app. Next, you’ll get to practice these concepts with hands-on exercises. You’ll try logging in, storing tokens, and protecting routes yourself. This will help you get comfortable with real-world authentication flows in React.

Great job making it this far — let’s keep going!

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