Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our last lesson, we successfully created a React context to bridge our server-side authentication state to the client-side UI. Now, both the server and the client are aware of the user's authentication status.
The next logical step is to use this server-side knowledge to enforce access control. How do we prevent an unauthenticated user from even seeing a protected page like /dashboard? How do we restrict an /admin area to users with a specific role?
This lesson will address exactly that, covering the learning outcome:
Implement protected routes using server-side authentication checks in route loaders or middleware.
We will explore the two primary, server-centric patterns for securing routes in Tanstack Start: using the beforeLoad route lifecycle hook and implementing dedicated authentication middleware. By the end, you'll be able to choose and implement the right strategy for different security requirements in your application.
1. Strategy 1: Route Protection with beforeLoad
The most direct way to protect a route in Tanstack Router is by using the beforeLoad function. This function is guaranteed to run on the server before the route component is rendered, making it the perfect place for an authentication check.
If the check fails, we can simply throw a redirect to send the user to a login page.
The official Tanstack Start documentation provides a clean and powerful pattern for this using layout routes.
Authentication | TanStack Start React Docs
The official Tanstack Start documentation demonstrates the canonical way to protect routes using beforeLoad on a layout route. This is the foundational pattern for route-level security.
Please read the section '4. Route Protection'. Pay close attention to the file routes/_authed.tsx. This is a layout route. Notice how beforeLoad calls a server function to get the user and throws a redirect if the user is not found. Also, see how the user data is returned and made available to child routes.
The Layout Route Pattern
As you saw in the documentation, the convention is to create a layout route (e.g., src/routes/_authed.tsx) that performs the authentication check. The leading underscore in _authed signifies that this is a layout route group and does not add a segment to the URL path.
Any route file placed within a corresponding src/routes/_authed/ directory will automatically become a child of this layout and inherit its protection.
Here is what the implementation looks like:
1. The Protected Layout Route (src/routes/_authed.tsx)
// src/routes/_authed.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAppSession } from '~/lib/session'; // Your session utility
export const Route = createFileRoute('/_authed')({
// This runs on the server before any child route attempts to load.
beforeLoad: async ({ location }) => {
const session = await getAppSession();
const sessionData = await session.get();
// If not authenticated, redirect to the login page.
if (sessionData?.status !== 'authenticated') {
throw redirect({
to: '/login',
// Optional: Pass the original intended URL to redirect back after login.
search: {
redirect: location.href,
},
});
}
// If authenticated, return the user data to the route's context.
return {
user: sessionData.user,
};
},
});
2. A Protected Route (src/routes/_authed/dashboard.tsx)
Because this file is inside the _authed directory, the beforeLoad check from the layout will run before this component can render.
// src/routes/_authed/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router';
// This route is automatically protected by its parent layout.
export const Route = createFileRoute('/_authed/dashboard')({
component: DashboardComponent,
});
function DashboardComponent() {
// We can access the user data returned from the layout's beforeLoad.
const { user } = Route.useRouteContext();
return (
<div>
<h3>Welcome to your Dashboard, {user.email}!</h3>
<p>This page is protected.</p>
</div>
);
}
This approach is elegant because it co-locates the protection logic with the routes themselves and provides a clear, hierarchical structure for your protected areas.
This video from Web Dev Cody provides a practical walkthrough of this exact concept.
How I Protect My Tanstack Start Applications
This video demonstrates the beforeLoad pattern in a real-world Tanstack Start application, showing how to redirect unauthenticated users away from an admin section.
Watch from 0:36 to 1:22. The presenter shows how a beforeLoad function on an /admin layout route is used to assert authentication and redirect if the user is not logged in or not an admin.
2. Extending to Authorization (Role-Based Access Control)
The same beforeLoad pattern can be easily extended from simple authentication (is the user logged in?) to more granular authorization (does the user have the required permissions?).
You simply add another check inside beforeLoad against the user's properties, such as a role.
Authentication | TanStack Start React Docs
The Tanstack Start docs also cover how to add role-based checks on top of the basic authentication check.
Now, read the 'Role-Based Access Control (RBAC)' section. It shows how you can create a separate layout route for /admin that first ensures the user is authenticated (by nesting under _authed) and then checks if they have the 'ADMIN' role.
For example, you could create an admin-only layout at src/routes/_authed/_admin.tsx:
// src/routes/_authed/_admin.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
// A helper function to check roles
function hasAdminRole(user: { roles: string[] }): boolean {
return user.roles.includes('admin');
}
export const Route = createFileRoute('/_authed/_admin')({
beforeLoad: async ({ context }) => {
// The `context.user` is available because this route is a child of `/_authed`.
const { user } = context;
if (!hasAdminRole(user)) {
// If the user is not an admin, redirect them away.
throw redirect({ to: '/dashboard', search: { error: 'unauthorized' } });
}
},
});
Any route inside src/routes/_authed/_admin/ would now require the user to be both authenticated and have the 'admin' role.
3. Strategy 2: Route Protection with Middleware
While beforeLoad is excellent for route-specific logic, sometimes you need a more reusable, cross-cutting solution. This is where middleware comes in. Middleware allows you to define a chain of functions that run before your route loaders and actions.
This is a more advanced feature of the underlying React Router and offers greater flexibility for complex applications.
The React Router documentation provides a comprehensive guide to middleware, explaining the core concepts and showing a clear pattern for authentication.
First, read the 'Quick Start (Framework mode)' section to understand the basic setup. Then, jump to the 'Common Patterns' section and read the 'Authentication' example. This shows how to create a reusable authMiddleware function.
Implementing Authentication Middleware
Following the pattern from the docs, the process looks like this:
1. Enable Middleware
In your vite.config.ts (or equivalent), you need to enable the feature flag for the Tanstack Router plugin:
// vite.config.ts
import { defineConfig } from 'vite';
import { tanstackRouter } from '@tanstack/router-vite-plugin';
export default defineConfig({
plugins: [
// ... other plugins
tanstackRouter({
future: {
v7_middleware: true,
},
}),
],
});
2. Create Reusable Middleware
Create a file for your middleware, for example src/middleware/auth.ts.
// src/middleware/auth.ts
import { redirect } from '@tanstack/react-router';
import { getAppSession } from '~/lib/session';
// This middleware function can be imported and used on any route.
export const authMiddleware = async () => {
const session = await getAppSession();
const sessionData = await session.get();
if (sessionData?.status !== 'authenticated') {
throw redirect({ to: '/login' });
}
// If we don't throw, the chain continues to the next middleware or the route loader.
};
3. Apply Middleware to a Route
You can now apply this middleware to any route by exporting it from the route file.
// src/routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router';
import { authMiddleware } from '~/middleware/auth';
export const Route = createFileRoute('/dashboard')({
// Apply the middleware
beforeLoad: authMiddleware,
component: DashboardComponent,
});
function DashboardComponent() {
// ...
}
An Important Consideration: When Middleware Runs
There is a critical detail about how server middleware behaves during client-side navigation.
Understanding the execution context of middleware is crucial to avoid security gaps. This section of the docs explains it perfectly.
Please read the subsection 'When Middleware Runs', focusing on the 'Server Middleware' part. The key takeaway is that server middleware only runs on client-side navigations if the target route has a loader or action that triggers a server request.
As the documentation explains, if you navigate from / to /dashboard on the client, and /dashboard has no loader, your authMiddleware will not run. This is because React Router optimizes by not making a network request if it doesn't need to fetch data.
To ensure your server-side authMiddleware always runs, you must add a loader to the route, even if it's empty:
// src/routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router';
import { authMiddleware } from '~/middleware/auth';
export const Route = createFileRoute('/dashboard')({
beforeLoad: authMiddleware,
// By adding a loader, we force the middleware to run on every navigation.
loader: () => {
return null;
},
component: DashboardComponent,
});
4. beforeLoad vs. Middleware: Which to Choose?
| Feature | beforeLoad on Layout Route |
Middleware |
|---|---|---|
| Simplicity | High. Integrated directly into the route definition. | Moderate. Requires a feature flag and understanding of the execution model. |
| Organization | Excellent for hierarchical protection (e.g., all routes in a folder). | Excellent for applying non-hierarchical, cross-cutting concerns. |
| Reusability | Logic is tied to the layout route. | Highly reusable. Define once, import, and apply anywhere. |
| Composition | Can be chained via nested layouts. | Designed for composition in an array: [logger, auth, featureFlagCheck]. |
| Gotchas | Relatively few. It behaves predictably. | Must ensure a loader exists for it to run on client-side navigations. |
Recommendation:
- For most standard application structures, the
beforeLoadon a layout route pattern is simpler, more explicit, and often the best choice. - If you have complex, non-hierarchical security rules or want to compose multiple cross-cutting concerns (like logging, A/B testing, and auth), middleware provides a more powerful and scalable abstraction.
Conclusion
You now have two robust, server-side patterns for implementing protected routes. By performing these checks on the server, you ensure that unauthorized users never receive the code or data for protected pages, which is fundamental for both security and performance in an SSR application.
Key Takeaways:
beforeLoadis the primary tool for route protection. Using it on a layout route (_authed) is a powerful and clean pattern for protecting entire sections of your app.throw redirect()is the mechanism for enforcing protection. It halts the rendering process on the server and sends the user to another page.- Middleware is a more advanced, reusable pattern for applying cross-cutting concerns, but requires careful attention to its execution rules, particularly during client-side navigation.
- Authorization is an extension of authentication. The same patterns can be used to check for specific user roles or permissions.
Next Up:
We have now covered the core concepts of building a fully authenticated SSR application with Tanstack Start. In the next module, we will begin the main project: migrating an existing Single-Page Application. Our first step will be to analyze an existing SPA to identify code dependent on client-side browser APIs (e.g., window, localStorage), which is a critical prerequisite for a successful SSR transformation.