Login Flow and Protected Routes
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)
-
Three
useStatehooksusername/setUsername: Holds the current value of the username input; updates on each keystroke so the form is fully controlled by React.password/setPassword: Same for the password field.error/setError: Keeps an error message to show above the form. Cleared before each attempt; set when the API rejects credentials or a network error occurs.
-
e.preventDefault()- Stops the browser from doing a full page reload on form submit.
- Lets React handle submission logic asynchronously (call API, update state, navigate) without leaving the SPA context.
-
apiClient.post- Sends a POST to
/auth/loginwith{ username, password }. - Returns the parsed envelope. We check
success === trueand then readdata.access_token.
- Sends a POST to
-
useNavigate()- Returns a function you can call to imperatively navigate after side effects (e.g., login).
navigate('/shelf', { replace: true })pushes a transition to/shelfand replaces the current history entry so the back button doesn’t take the user back to the login page.
-
Token handling
- On success, we persist the token via
saveToken(token)tolocalStorageso the user stays logged in across refreshes. - On failure, we set a user-friendly error string that renders in the alert area.
- On success, we persist the token via
This makes the login flow predictable, testable, and aligned with the backend’s response contract.
