
Build Your First Secure Full-Stack App: ASP.NET Core + React with JWT Auth
Welcome! If you're ready to dive into full-stack web development, connecting a backend API to a frontend user interface, you're in the right place. This tutorial will guide you step-by-step in building a basic secure application featuring:
- A backend API using ASP.NET Core (C#).
- A simple database using SQLite and Entity Framework Core.
- User registration and login using JWT (JSON Web Tokens) for authentication.
- Secured API endpoints requiring a valid token.
- A frontend user interface using React.
- Navigation and protected routes handled by TanStack Router.
- Clear separation between public pages (like Home, Login, Register) and secured pages (like a Dashboard).
We'll set up everything using VS Code, a popular and powerful code editor for full-stack development.
This tutorial covers the core concepts and essential code. For the complete, ready-to-run project code, check out the full GitHub repository here: [Link to your GitHub Repository Here]
What You'll Need (Prerequisites):
- VS Code: Download and install from https://code.visualstudio.com/.
- .NET SDK: Download and install the latest version from https://dotnet.microsoft.com/download. This includes the .NET runtime and command-line tools.
- Node.js and npm: Download and install the latest LTS version from https://nodejs.org/. npm (Node Package Manager) is included with Node.js.
Setting Up Your Development Environment (VS Code)
Once you have VS Code, the .NET SDK, and Node.js installed, let's set up VS Code for a better full-stack experience:
- Open VS Code.
- Go to the Extensions view (Ctrl+Shift+X or Cmd+Shift+X).
- Search for and install these recommended extensions:
- C# by Microsoft: Provides rich language support for C# and ASP.NET Core.
- ES7+ React/Redux/GraphQL/React-Native snippets by dsznajder: Adds helpful code snippets for React.
- Prettier - Code formatter by Prettier: Automatically formats your code for consistency.
- ESLint by Dirk Baeumer: Integrates ESLint to find and fix problems in your JavaScript/React code.
- You might also find GitLens helpful for working with Git repositories.
That's it! VS Code is now ready to handle both your ASP.NET Core backend and your React frontend.
Part 1: Building the ASP.NET Core Backend (API)
This part will handle our data, user accounts, and issuing authentication tokens.
Step 1: Create the Project
Open your terminal or command prompt and navigate to the folder where you want to create your project. Run:
1dotnet new webapi -n MySecureApi2cd MySecureApi3code .
dotnet new webapi -n MySecureApi
creates a new ASP.NET Core Web API project namedMySecureApi
.cd MySecureApi
moves you into the new project directory.code .
opens the project in VS Code.
Step 2: Install NuGet Packages
We need packages for SQLite database access using Entity Framework Core and for JWT authentication. Open the VS Code terminal (Ctrl+ or Cmd+
). Make sure you are in the MySecureApi
directory.
1dotnet add package Microsoft.EntityFrameworkCore.Sqlite2dotnet add package Microsoft.EntityFrameworkCore.Design3dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Step 3: Configure appsettings.json
We'll store our database connection string and JWT settings here. Open appsettings.Development.json
(it's good practice to keep secrets out of the main appsettings.json
for source control, though for a tutorial, placing them here is fine).
1// appsettings.Development.json2{3 "Logging": { /* ... */ },4 "AllowedHosts": "*",5 "ConnectionStrings": {6 "DefaultConnection": "Data Source=mydatabase.db" // SQLite stores data in a file7 },8 "Jwt": {9 "Key": "YOUR_SUPER_SECURE_RANDOM_KEY_AT_LEAST_16_CHARS_LONG", // !!! CHANGE THIS !!! Must be strong and secret10 "Issuer": "YourApiIssuer", // e.g., your domain name11 "Audience": "YourApiClient" // e.g., your frontend URL or app name12 }13}
Step 4: Create User Model
Create a folder named Models
in the root of MySecureApi
. Inside Models
, create a file User.cs
.
1// Models/User.cs2using System.ComponentModel.DataAnnotations;34namespace MySecureApi.Models5{6 public class User7 {8 [Key] // Makes Id the primary key9 public int Id { get; set; }10 [Required] // Makes Username a required field11 public string Username { get; set; } = string.Empty; // Use string.Empty to avoid null12 [Required]13 public byte[] PasswordHash { get; set; } = new byte[0]; // Store hashed password14 [Required]15 public byte[] PasswordSalt { get; set; } = new byte[0]; // Store random salt16 // Simple role for authorization17 public string Role { get; set; } = "User"; // Default role for new users18 }19}
Step 5: Create Database Context
Entity Framework Core uses a DbContext
to interact with the database. Create a folder Data
and add a file DataContext.cs
.
1// Data/DataContext.cs2using Microsoft.EntityFrameworkCore;3using MySecureApi.Models; // Make sure to use the correct namespace45namespace MySecureApi.Data6{7 public class DataContext : DbContext8 {9 // Constructor to accept options (needed for dependency injection)10 public DataContext(DbContextOptions<DataContext> options) : base(options) { }1112 // DbSet represents the Users table in the database13 public DbSet<User> Users { get; set; }1415 // OnConfiguring is not typically needed if using AddDbContext in Program.cs16 // but can be useful for simple setups or logging.17 // If you configure the connection string in Program.cs, you can remove this method.18 // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)19 // {20 // // Example: If not using appsettings.json or Program.cs config21 // // optionsBuilder.UseSqlite("Data Source=mydatabase.db");22 // }23 }24}
Step 6: Configure DB Context in Program.cs
Open Program.cs
. Add the DataContext
to the service collection and configure it to use SQLite with the connection string from appsettings.Development.json
. Add the necessary using
statements at the top.
1// Program.cs (Add these using statements at the top)2using Microsoft.EntityFrameworkCore;3using MySecureApi.Data; // Use your Data namespace45var builder = WebApplication.CreateBuilder(args);67// Add services to the container.89// Configure DataContext to use SQLite10builder.Services.AddDbContext<DataContext>(options =>11{12 // Get the connection string from appsettings.json13 options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));14});1516// ... rest of Program.cs
Step 7: Create and Apply Database Migrations
Migrations translate your C# models into database schema. Open the VS Code terminal in the MySecureApi
directory.
1dotnet ef migrations add InitialCreate2dotnet ef database update
dotnet ef migrations add InitialCreate
creates a snapshot of your current models and generates code to create the database schema (a migration file).dotnet ef database update
applies any pending migrations, creating themydatabase.db
file and theUsers
table.
You should now see a mydatabase.db
file created in your project root.
Step 8: Configure JWT Authentication & Authorization
In Program.cs
, add the services and middleware for JWT authentication and authorization. Add necessary using
statements for JWT.
1// Program.cs (Add these using statements at the top)2using System.Text; // Needed for Encoding3using Microsoft.AspNetCore.Authentication.JwtBearer; // Needed for JWT Auth4using Microsoft.IdentityModel.Tokens; // Needed for SymmetricSecurityKey56// ... AddDbContext code ...78// Configure JWT Authentication9builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)10 .AddJwtBearer(options => {11 options.TokenValidationParameters = new TokenValidationParameters12 {13 // Validate the signing key using the secret key from appsettings14 ValidateIssuerSigningKey = true,15 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),1617 // Validate the issuer (who created the token)18 ValidateIssuer = true,19 ValidIssuer = builder.Configuration["Jwt:Issuer"],2021 // Validate the audience (who the token is for)22 ValidateAudience = true,23 ValidAudience = builder.Configuration["Jwt:Audience"],2425 // Validate the token's lifetime (expiration)26 ValidateLifetime = true27 };28 });2930builder.Services.AddAuthorization(); // Add authorization services3132// ... other builder services ...3334var app = builder.Build();3536// ... app.UseHttpsRedirection(); etc ...3738// IMPORTANT: Authentication and Authorization middleware MUST be added here, AFTER routing and CORS39app.UseAuthentication();40app.UseAuthorization();4142// ... app.MapControllers(); etc ...
Step 9: Add CORS Configuration
Your React frontend will run on a different port (e.g., 3000 or 5173) than your backend. CORS (Cross-Origin Resource Sharing) must be configured to allow requests from the frontend to the backend. Add this to Program.cs
.
1// Program.cs (Add using Microsoft.AspNetCore.Cors;)23var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; // Define a policy name45builder.Services.AddCors(options =>6{7 options.AddPolicy(MyAllowSpecificOrigins,8 builder =>9 {10 // Replace with the actual origin(s) where your frontend will run11 builder.WithOrigins("http://localhost:3000", "http://localhost:5173")12 .AllowAnyHeader() // Allow all common headers13 .AllowAnyMethod() // Allow GET, POST, PUT, DELETE, etc.14 .AllowCredentials(); // Needed if you handle cookies/credentials (good practice)15 });16});1718var app = builder.Build();1920// ... app.UseHttpsRedirection();2122// Add CORS middleware AFTER routing, but BEFORE Auth/Auth23app.UseCors(MyAllowSpecificOrigins);2425app.UseAuthentication();26app.UseAuthorization();2728// ... app.MapControllers();
Step 10: Create Authentication Controller (AuthController.cs
)
This controller will have endpoints for /api/auth/register
and /api/auth/login
. You'll need helper methods for securely handling passwords (hashing and verifying) and generating JWTs. For this tutorial, we'll show the controller structure; find the full helper method implementations in the linked repository.
Create a folder Controllers
and add AuthController.cs
.
1// Controllers/AuthController.cs2using Microsoft.AspNetCore.Mvc;3using MySecureApi.Data;4using MySecureApi.Models;5using Microsoft.IdentityModel.Tokens;6using System.IdentityModel.Tokens.Jwt;7using System.Security.Claims;8using System.Text;9using Microsoft.Extensions.Configuration;10using System.Security.Cryptography; // For password hashing11using System.Threading.Tasks;12using Microsoft.EntityFrameworkCore; // Needed for .AnyAsync()1314namespace MySecureApi.Controllers15{16 // Define input models (DTOs - Data Transfer Objects)17 public class UserRegistrationDto18 {19 public string Username { get; set; } = string.Empty;20 public string Password { get; set; } = string.Empty;21 }2223 public class UserLoginDto24 {25 public string Username { get; set; } = string.Empty;26 public string Password { get; set; } = string.Empty;27 }2829 [Route("api/[controller]")] // Base route is /api/auth30 [ApiController]31 public class AuthController : ControllerBase32 {33 private readonly DataContext _context;34 private readonly IConfiguration _configuration;3536 public AuthController(DataContext context, IConfiguration configuration)37 {38 _context = context;39 _configuration = configuration; // Access JWT settings from appsettings40 }4142 // POST: api/auth/register43 [HttpPost("register")]44 public async Task<IActionResult> Register([FromBody] UserRegistrationDto request)45 {46 // Basic validation47 if (request.Username == null || request.Password == null)48 {49 return BadRequest("Username and password are required.");50 }5152 // Check if user already exists53 if (await _context.Users.AnyAsync(u => u.Username == request.Username))54 {55 return BadRequest("Username already exists.");56 }5758 // Create password hash and salt (SEE HELPER METHOD IN FULL REPO)59 CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);6061 var newUser = new User62 {63 Username = request.Username,64 PasswordHash = passwordHash,65 PasswordSalt = passwordSalt,66 Role = "User" // Assign default role67 };6869 _context.Users.Add(newUser);70 await _context.SaveChangesAsync();7172 return Ok("User registered successfully!");73 }7475 // POST: api/auth/login76 [HttpPost("login")]77 public async Task<IActionResult> Login([FromBody] UserLoginDto request)78 {79 // Basic validation80 if (request.Username == null || request.Password == null)81 {82 return BadRequest("Username and password are required.");83 }8485 // Find user by username86 var user = await _context.Users.FirstOrDefaultAsync(u => u.Username == request.Username);8788 // Check if user exists and password is correct89 if (user == null || !VerifyPasswordHash(request.Password, user.PasswordHash, user.PasswordSalt))90 {91 return Unauthorized("Invalid credentials."); // Use Unauthorized for login failures92 }9394 // Generate JWT Token (SEE HELPER METHOD IN FULL REPO)95 string token = CreateToken(user);9697 // Return the token to the client98 return Ok(new { token = token });99 }100101 // --- Helper Methods (See Full Repo for implementations) ---102103 // Creates a password hash and salt104 private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { /* ... */ }105106 // Verifies a plain password against a hash and salt107 private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) { /* ... */ }108109 // Creates a JWT token for a given user110 private string CreateToken(User user) { /* ... */ }111 }112}
Step 11: Create Secured Controller (SecuredController.cs
)
This controller will contain actions that can only be accessed by authenticated users (or users with specific roles). Create a file SecuredController.cs
in the Controllers
folder.
1// Controllers/SecuredController.cs2using Microsoft.AspNetCore.Authorization; // Needed for [Authorize]3using Microsoft.AspNetCore.Mvc;4using System.Security.Claims; // Needed to access user claims56// Apply [Authorize] at the controller level to protect all actions within it by default7[Authorize]8[Route("api/[controller]")] // Base route is /api/secured9[ApiController]10public class SecuredController : ControllerBase11{12 // GET: api/secured13 // This action requires any authenticated user (because of the [Authorize] on the controller)14 [HttpGet]15 public IActionResult GetSecuredData()16 {17 // Access claims from the authenticated user's JWT token18 var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; // Get the user ID claim (often added in CreateToken)19 var userName = User.FindFirst(ClaimTypes.Name)?.Value; // Get the username claim20 var userRole = User.FindFirst(ClaimTypes.Role)?.Value; // Get the role claim2122 return Ok($"Hello, {userName ?? 'Authenticated User'}! You accessed secured data. Your User ID: {userId}, Role: {userRole ?? 'None'}.");23 }2425 // GET: api/secured/admin26 // This action requires an authenticated user AND they must have the "Admin" role claim27 [HttpGet("admin")]28 [Authorize(Roles = "Admin")] // Override controller's Authorize to add role requirement29 public IActionResult GetAdminData()30 {31 return Ok("This data is only accessible to users with the 'Admin' role!");32 }3334 // Example of an action that requires *only* the controller-level auth (already implicit)35 // [HttpGet("another-secured-endpoint")]36 // public IActionResult AnotherSecuredAction()37 // {38 // return Ok("Another endpoint requiring authentication.");39 // }40}
Your backend API is now set up with user management, JWT issuance, and endpoint protection!
Part 2: Building the React Frontend with TanStack Router
Now, let's create the React application that consumes your secure API.
Step 12: Create the React Project
Open a new terminal or command prompt window (keep the backend terminal running). Navigate to the folder where you want to create your frontend project (e.g., alongside your MySecureApi
folder). We'll use Vite for a fast setup.
1npm create vite@latest my-secure-app --template react2cd my-secure-app3npm install4code .
npm create vite@latest my-secure-app --template react
creates a new React project using Vite.cd my-secure-app
moves into the new project folder.code .
opens the frontend project in VS Code.
Step 13: Install npm Packages
We need Axios for making API calls and @tanstack/react-router
for routing.
1npm install axios @tanstack/react-router
Step 14: Create API Client (api.js
)
This file will contain our Axios instance configured to talk to the backend API and automatically attach the JWT token to requests. Create a file src/api.js
.
1// src/api.js2import axios from 'axios';34// Create an Axios instance with your backend API base URL5const api = axios.create({6 // !! Update with the actual URL where your backend runs !!7 // Usually https://localhost:7xxx or http://localhost:5xxx during development8 baseURL: 'https://localhost:7100/api',9});1011// --- Request Interceptor ---12// This runs BEFORE each request leaves the client13api.interceptors.request.use(14 config => {15 // Get the JWT token from local storage (or wherever you stored it)16 const token = localStorage.getItem('jwtToken');1718 // If a token exists, add it to the Authorization header19 if (token) {20 config.headers['Authorization'] = `Bearer ${token}`;21 }2223 // Return the modified config24 return config;25 },26 error => {27 // Handle request errors (e.g., network issues)28 return Promise.reject(error);29 }30);3132// --- Response Interceptor (Optional but Recommended) ---33// This runs AFTER a response is received but BEFORE it's passed to your code34// Good for handling token expiration or other global errors35api.interceptors.response.use(36 response => {37 return response; // Just pass through successful responses38 },39 error => {40 // If the response is 401 Unauthorized (e.g., token expired)41 if (error.response && error.response.status === 401) {42 console.log("Unauthorized! Token might be expired or invalid. Logging out...");43 // Clear token and redirect to login (implement logout logic)44 localStorage.removeItem('jwtToken');45 // You would typically trigger a state update or redirect here46 // In a real app, this might trigger a global auth context update47 // For now, we'll let the ProtectedRoute handle the redirect on the next render48 }49 return Promise.reject(error); // Propagate the error50 }51);525354export default api; // Export the configured instance
Step 15: Create Authentication Context (AuthContext.js
)
We'll use React Context to manage the user's login state across the application. Create a file src/AuthContext.js
.
1// src/AuthContext.js2import React, { createContext, useState, useContext, useEffect } from 'react';3import api from './api'; // Import your configured api client45// Create the Context6const AuthContext = createContext(null);78// Create a Provider component9export const AuthProvider = ({ children }) => {10 // State to hold the user object (null if not logged in)11 const [user, setUser] = useState(null);12 // State to indicate if the initial loading check is complete13 const [loading, setLoading] = useState(true);1415 // Check for existing token when the app starts16 useEffect(() => {17 const token = localStorage.getItem('jwtToken');18 if (token) {19 // --- IMPORTANT FOR A REAL APP ---20 // In a real application, you should NOT assume the token is valid just because it exists.21 // You should ideally call a secure backend endpoint (e.g., /api/auth/me or /api/auth/verify)22 // that validates the token on the server and returns user details.23 // For this boilerplate, we'll just set a basic user object if a token exists24 // and rely on API calls failing (handled by interceptor) for invalid tokens.25 try {26 // Decode the token locally to get basic info like username (OPTIONAL & INSECURE for sensitive data)27 // For a simple example, you can just set a placeholder28 const base64Url = token.split('.')[1];29 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');30 const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {31 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);32 }).join(''));33 const claims = JSON.parse(jsonPayload);3435 // Assuming username is stored in 'sub' or 'name' claim (check your backend CreateToken method)36 const username = claims.sub || claims.name || 'AuthenticatedUser';37 const role = claims.role || 'User'; // Assuming role is stored in 'role' claim3839 setUser({ username, role }); // Set user state based on token claims40 console.log("Found token, attempting to use:", claims);4142 } catch (error) {43 console.error("Failed to decode or process token:", error);44 localStorage.removeItem('jwtToken'); // Clear invalid token45 setUser(null);46 }4748 }49 setLoading(false); // Loading is complete50 }, []); // Run only once on mount5152 // Login function: Calls backend, stores token, updates state53 const login = async (username, password) => {54 try {55 const response = await api.post('/auth/login', { username, password });56 const token = response.data.token;57 localStorage.setItem('jwtToken', token); // Store the token5859 // --- IMPORTANT FOR A REAL APP ---60 // Ideally, you'd get user details (like roles) from the login response61 // For this boilerplate, we'll decode the token locally again62 const base64Url = token.split('.')[1];63 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');64 const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {65 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);66 }).join(''));67 const claims = JSON.parse(jsonPayload);6869 const userName = claims.sub || claims.name || 'AuthenticatedUser';70 const role = claims.role || 'User'; // Assuming role is stored in 'role' claim7172 setUser({ username: userName, role }); // Update user state7374 console.log('Login successful, token stored.');75 return true; // Indicate success76 } catch (error) {77 console.error('Login failed:', error.response?.data || error.message);78 // Handle login errors (e.g., display error message in UI)79 setUser(null); // Ensure user is null on failed login80 localStorage.removeItem('jwtToken'); // Clear any potential partial tokens81 return false; // Indicate failure82 }83 };8485 // Logout function: Removes token, clears state86 const logout = () => {87 localStorage.removeItem('jwtToken'); // Remove token from storage88 setUser(null); // Clear user state89 console.log('Logged out, token removed.');90 // TanStack Router can be used for navigation here if needed:91 // const navigate = useNavigate(); navigate('/login'); // Requires hook usage92 };9394 // Provide the user state, login, and logout functions to children95 return (96 <AuthContext.Provider value={{ user, login, logout, loading }}>97 {!loading && children} {/* Render children only after initial check */}98 </AuthContext.Provider>99 );100};101102// Custom hook to easily access the auth context103export const useAuth = () => {104 const context = useContext(AuthContext);105 if (context === null) {106 // This means useAuth was called outside of the AuthProvider107 throw new Error('useAuth must be used within an AuthProvider');108 }109 return context;110};
Step 16: Define TanStack Router Routes
Create a folder src/routes
. Inside this folder, create the following files. We'll use the lazy loading pattern (.lazy.jsx
suffix) which is standard for components rendered by the router.
1// src/routes/root.lazy.jsx2import { createLazyFileRoute, Outlet } from '@tanstack/react-router';3import { AuthProvider } from '../AuthContext'; // Your Auth Context Provider45// This is the root layout route. It wraps the entire application.6export const Route = createLazyFileRoute('/')({7 component: () => (8 // Wrap the whole application with the AuthProvider so all routes can access auth context9 <AuthProvider>10 {/* Optional: Add a site-wide navigation menu here */}11 <nav style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>12 <Link to="/">Home</Link> |{' '}13 <Link to="/login">Login</Link> |{' '}14 <Link to="/register">Register</Link> |{' '}15 {/* Add conditional links for authenticated users later if desired */}16 <Link to="/dashboard">Dashboard</Link> |{' '}17 <Link to="/secured">Secured Data</Link>18 <AuthStatusDisplay /> {/* Component to show login/logout button */}19 </nav>2021 {/* The Outlet component renders the currently matched child route */}22 <div style={{ padding: '10px' }}>23 <Outlet />24 </div>25 </AuthProvider>26 ),27});282930// Create a simple component to display auth status and logout button31// src/components/AuthStatusDisplay.jsx32import React from 'react';33import { useAuth } from '../AuthContext';34import { Link, useNavigate } from '@tanstack/react-router';3536function AuthStatusDisplay() {37 const { user, logout, loading } = useAuth();38 const navigate = useNavigate();3940 if (loading) return <div>Loading Auth...</div>; // Or null4142 if (user) {43 return (44 <span>45 | Logged in as: {user.username} (<button onClick={() => { logout(); navigate('/'); }}>Logout</button>)46 </span>47 );48 } else {49 // Links are already in the main nav, but you could add them here too50 return <span>| Not logged in</span>;51 }52}535455// src/routes/index.lazy.jsx (Public Home Page)56import { createLazyFileRoute } from '@tanstack/react-router';5758export const Route = createLazyFileRoute('/')({59 component: () => {60 // This component renders when the URL is exactly "/"61 return (62 <div>63 <h2>Welcome!</h2>64 <p>This is the public landing page.</p>65 <p>Explore the navigation above.</p>66 </div>67 );68 },69});7071// src/routes/login.lazy.jsx (Public Login Page)72// You already drafted the component logic in the previous response.73// Use that component logic here. It needs useState, useNavigate, useAuth, api.74import { createLazyFileRoute } from '@tanstack/react-router';75// Import the actual Login component logic you create separately76// import LoginPageContent from '../pages/LoginPageContent';7778export const Route = createLazyFileRoute('/login')({79 component: () => (80 // <LoginPageContent /> // Render your Login form component here81 <div>Login Form Goes Here (See full repo)</div>82 ),83});8485// src/routes/register.lazy.jsx (Public Register Page)86// Similar to login, use your Register component logic here.87import { createLazyFileRoute } from '@tanstack/react-router';88// import RegisterPageContent from '../pages/RegisterPageContent';8990export const Route = createLazyFileRoute('/register')({91 component: () => (92 // <RegisterPageContent /> // Render your Register form component here93 <div>Register Form Goes Here (See full repo)</div>94 ),95});969798// src/routes/_auth.lazy.jsx (Layout Route for Authenticated Pages)99// This route acts as a parent for all routes that require authentication.100import { createLazyFileRoute, Outlet, redirect } from '@tanstack/react-router';101import { useAuth } from '../AuthContext'; // Get the authentication state102103export const Route = createLazyFileRoute('/_auth')({104 // The beforeLoad function runs BEFORE the route (or its children) are loaded.105 beforeLoad: ({ location }) => {106 // Access the auth state using your hook107 const { user, loading } = useAuth();108109 // If still loading auth state, prevent loading immediately.110 // TanStack Router can wait for promises, or you can handle this loading state in components.111 // For this simple redirect example, we proceed only after loading is false.112 if (loading) {113 // If you wanted to show a global loading spinner, you might return a promise here114 // that resolves when loading is done. For now, we'll just let it render and115 // the component can show its own loading state if needed.116 return; // Allow route to proceed while loading is true117 }118119 // If the user is NOT authenticated (user is null) after loading is done120 if (!user) {121 console.log("User not authenticated, redirecting to login.");122 // Throw a redirect response. TanStack Router catches this and navigates.123 throw redirect({124 to: '/login', // Redirect destination125 // Optional: Pass the original location so the user can be redirected back after login126 search: {127 redirect: location.href,128 },129 });130 }131132 // If authenticated (user is not null), allow the route and its children to load133 console.log("User authenticated, allowing access.");134 return;135 },136 // This component acts as a layout for all authenticated pages137 component: () => (138 <div>139 <h3>Secured Area</h3>140 {/* Optional: Add a sub-navigation menu for the secured area */}141 <nav style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>142 <Link to="/dashboard">Dashboard</Link> |{' '}143 <Link to="/secured">View Secured Data</Link>144 </nav>145 <div style={{ padding: '10px', border: '1px dashed green', margin: '10px 0' }}>146 <Outlet /> {/* Render the matched child route (dashboard or secured) */}147 </div>148 </div>149 ),150});151152// src/routes/_auth/dashboard.lazy.jsx (Secured Dashboard Page)153// This route is a child of the /_auth route, so it inherits the authentication check.154import { createLazyFileRoute } from '@tanstack/react-router';155import { useAuth } from '../../AuthContext'; // Get the authenticated user156157export const Route = createLazyFileRoute('/_auth/dashboard')({158 component: () => {159 const { user } = useAuth(); // Access the user object from context160 return (161 <div>162 <h2>Dashboard</h2>163 {user && <p>Welcome, {user.username}! You are logged in.</p>}164 <p>This content is only visible to authenticated users.</p>165 {/* Add dashboard-specific content here */}166 </div>167 );168 },169});170171// src/routes/_auth/secured.lazy.jsx (Another Secured Page)172// Also a child of /_auth, automatically protected.173import { createLazyFileRoute } from '@tanstack/react-router';174import { useState, useEffect } from 'react';175import api from '../../api'; // Your configured api client176177export const Route = createLazyFileRoute('/_auth/secured')({178 component: () => {179 const [data, setData] = useState(null);180 const [error, setError] = useState('');181 const [loading, setLoading] = useState(true);182183 useEffect(() => {184 const fetchData = async () => {185 try {186 // Make a call to the secured backend endpoint187 const response = await api.get('/secured');188 setData(response.data);189 setError('');190 } catch (err) {191 console.error('Error fetching secured data:', err);192 // The response interceptor might handle 401, but catch others here193 setError('Failed to fetch secured data.');194 setData(null); // Clear previous data on error195 } finally {196 setLoading(false);197 }198 };199 fetchData();200 }, []); // Empty dependency array means run once on mount201202 return (203 <div>204 <h2>Secured Data Page</h2>205 <p>This page fetches data from a protected API endpoint.</p>206 {loading && <p>Loading data...</p>}207 {error && <p style={{ color: 'red' }}>{error}</p>}208 {data && <p>API Response: **{data}**</p>}209 </div>210 );211 },212});
Step 17: Generate the TanStack Router Tree
Open the VS Code terminal in your my-secure-app
directory and run:
1npx @tanstack/router-cli generate
This command reads your src/routes
files and generates src/routeTree.gen.ts
(or .js
). This file defines the structure for the router and provides type safety if you're using TypeScript. You shouldn't edit this file manually.
Step 18: Configure and Use the Router in App.jsx
Now, set up the main router instance in your src/App.jsx
.
1// src/App.jsx2import React from 'react';3import { RouterProvider, createRouter } from '@tanstack/react-router';4// Import the generated route tree5import { routeTree } from './routeTree.gen';67// Create a router instance8const router = createRouter({ routeTree });910// Register the router instance for type safety (optional but recommended for TypeScript)11// You can remove this block if you are not using TypeScript12declare module '@tanstack/react-router' {13 interface Register {14 router: typeof router;15 }16}1718function App() {19 return (20 // Provide the router instance to the application21 <RouterProvider router={router} />22 );23}2425export default App;
Your React application is now set up with TanStack Router, JWT token handling via Axios, and protected routes using the beforeLoad
hook and your authentication context!
Running the Complete Application
You need to run both the backend API and the React frontend simultaneously.
- Start the Backend API: Open a terminal in your
MySecureApi
folder and run:
Your API should start and indicate the URLs it's listening on.1dotnet run --urls="https://localhost:7100;http://localhost:5000" # Or your desired ports - Start the React Frontend: Open a different terminal in your
my-secure-app
folder and run:
Your React app should open in your web browser (usually1npm run dev # If using Vite2# or3npm start # If using Create React Apphttp://localhost:5173
for Vite orhttp://localhost:3000
for CRA).
Testing the Authentication Flow:
- Access the application in your browser. You should see the Home page.
- Try navigating to "Dashboard" or "Secured Data". You should be automatically redirected to the "Login" page by TanStack Router's
beforeLoad
hook. - Go to the "Register" page and create a new user.
- Go to the "Login" page and log in with the user you just created. You should be redirected to the Dashboard.
- Now, try accessing the "Secured Data" page again. It should load successfully and display the data fetched from your backend's
/api/secured
endpoint. - Click the "Logout" button. You should be logged out and redirected, and accessing secured pages should redirect you back to Login.