Hello! Let's dive into our next lesson on JWT authentication.
Introduction
In our previous lesson, we successfully created a server function to handle logins and store a JWT securely in an HTTP-only cookie. This was a crucial first step, but the token is currently just sitting in the user's browser. It doesn't yet do anything.
This lesson directly addresses the learning outcome: Implement server-side token validation in middleware to protect API endpoints and server functions.
Our goal is to build a "gatekeeper" on the server. This gatekeeper will inspect every incoming request for a valid JWT before allowing access to protected areas. We will explore the two primary mechanisms TanStack Start provides for this:
- Route-level protection using the
beforeLoadoption to secure entire pages or sections of your application. - Server function middleware to protect individual API-like endpoints from unauthorized execution.
By the end of this lesson, you'll have a robust system for ensuring only authenticated users can access sensitive routes and perform restricted actions.
1. The Gatekeeper: Middleware in TanStack Start
In web development, "middleware" is code that runs between a server receiving a request and your main logic processing it. It's the perfect place for cross-cutting concerns like authentication, logging, or data parsing.
TanStack Start, building on Vinxi and TanStack Router, provides powerful, type-safe ways to implement this pattern. We'll focus on two key approaches.
2. Protecting Routes with beforeLoad
The most common requirement is to protect entire pages. For example, a /dashboard or /admin section should only be accessible to logged-in users. TanStack Router provides the beforeLoad option on routes, which acts as a powerful, built-in middleware.
Authenticated Routes | TanStack Router React Docs
First, let's understand the beforeLoad function from the official TanStack Router documentation. This will explain its purpose and how it fits into the route loading lifecycle.
Read the sections 'The route.beforeLoad Option' and 'Redirecting'. Pay close attention to two key ideas: that beforeLoad runs before its children, and that you can throw redirect() to handle unauthenticated users.
As the documentation states, beforeLoad is ideal for authentication checks. If the check fails, we can throw a redirect, sending the user to the login page. A common and effective pattern is to create a layout route that performs this check, automatically protecting all child routes nested within it.
For instance, you could create a file at src/routes/_authenticated.tsx. The _ prefix in the filename makes it a layout route that doesn't add a segment to the URL path. Any route defined inside an _authenticated folder (e.g., src/routes/_authenticated/dashboard.tsx) will now be a child of this layout and will have its beforeLoad check executed first.
The Validation Logic
Inside beforeLoad, we need to perform the actual token validation. This involves:
- Reading the
auth_tokencookie from the request. - Verifying the token's signature and expiration date using our JWT secret.
Let's look at a practical demonstration of this.
How I Protect My Tanstack Start Applications
This video by Web Dev Cody shows a real-world implementation of route protection in a TanStack Start application. He uses a layout route and a beforeLoad function, just as we've discussed.
Watch the segment from 00:36 to 01:40 to see how an /admin layout route is protected. Then, watch from 05:50 to 06:58, where he explains the validateRequest function that reads the cookie and validates the session.
Example Implementation
Let's combine these ideas into a complete example. First, we'll create a reusable server-side helper for token validation. This keeps our code DRY (Don't Repeat Yourself).
// src/lib/auth.server.ts
import "server-only"; // Ensures this code never runs on the client
import { getCookie } from "vinxi/http";
import jwt from "jsonwebtoken";
interface UserPayload {
userId: string;
role: string;
iat: number;
exp: number;
}
export function validateToken(token: string): UserPayload | null {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload;
return decoded;
} catch (error) {
// Token is invalid (bad signature, expired, etc.)
return null;
}
}
export function getAuthenticatedUser() {
const token = getCookie("auth_token");
if (!token) {
return null;
}
return validateToken(token);
}
Note: The try...catch block is essential. jwt.verify will throw an error for an invalid or expired token, which is the behavior we want to catch.
Now, we can use this helper in our protected layout route:
// src/routes/_authenticated.tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { getAuthenticatedUser } from "~/lib/auth.server";
export const Route = createFileRoute("/_authenticated")({
beforeLoad: ({ location }) => {
// 1. Check for authenticated user
const user = getAuthenticatedUser();
// 2. If no user, redirect to login
if (!user) {
throw redirect({
to: "/login",
search: {
// Preserve the original destination for a seamless redirect after login
redirect: location.href,
},
});
}
// 3. If user is authenticated, proceed to load the route
},
component: () => <Outlet />, // Renders the matched child route
});
With this in place, any route inside the src/routes/_authenticated/ directory is now protected. Any attempt to access it without a valid JWT will result in a redirect to /login.
3. Protecting Server Functions
Protecting pages is only half the battle. Your application will also have server functions (server$) that perform mutations, like updating a user profile or submitting a form. These are effectively API endpoints and must be secured independently. A malicious user could try to call a server function directly, bypassing the page UI.
TanStack Start provides an elegant middleware system for server functions.
How I Protect My Tanstack Start Applications
Let's return to the Web Dev Cody video. He provides an excellent explanation of how to attach middleware to server functions to enforce authentication.
Watch from 03:16 to 04:42. Focus on how he uses authenticated.use(...) to wrap a server function. Notice how the middleware not only protects the function but also passes the user context to it.
Example Implementation
Let's create a reusable middleware. This middleware will use our getAuthenticatedUser helper. A powerful feature of this pattern is the ability to pass context (like the validated user payload) to the wrapped server function, making it available for your business logic.
// src/lib/auth.server.ts (continued)
import { createMiddleware } from "@tanstack/react-start/server";
// Middleware to ensure a user is authenticated
export const authenticated = createMiddleware({
onEnter: async (ctx) => {
const user = getAuthenticatedUser();
if (!user) {
throw new Error("UNAUTHORIZED"); // Or a more specific error
}
// Attach the user payload to the context for the next function
return { user };
},
});
Now, you can easily protect any server function by applying this middleware.
// src/routes/profile/update-profile.server.ts
import { server$ } from "@tanstack/react-start/server";
import { authenticated } from "~/lib/auth.server";
import { db } from "~/db"; // Your hypothetical database client
// Use the middleware to protect this function
export const updateProfile = authenticated.use(server$(async function (data: { name: string }) {
// Thanks to the middleware, we have type-safe access to the user context
const { user } = this.ctx;
console.log(`User ${user.userId} is updating their profile.`);
// Now you can safely perform the database update
await db.user.update({
where: { id: user.userId },
data: { name: data.name },
});
return { success: true };
}));
If an unauthenticated user attempts to call updateProfile, the authenticated middleware will run, find no valid user, and throw an "UNAUTHORIZED" error, preventing the function's logic from ever executing.
This pattern is extremely powerful. It's declarative, reusable, and ensures that your protected functions always have the user context they need in a type-safe way.
Conclusion
In this lesson, we built the core security layer for our application by implementing server-side token validation.
Key Takeaways:
beforeLoadfor Routes: Use this on a layout route (e.g.,_authenticated.tsx) to protect entire sections of your app. If authentication fails,throw redirect()to send the user to a login page.- Middleware for Server Functions: Use
createMiddlewareto build reusable authentication checks for yourserver$functions. This protects your API endpoints from unauthorized access. - Abstract Validation Logic: Create a central, server-only helper function (e.g.,
getAuthenticatedUser) to handle cookie parsing and JWT verification. This keeps your code clean and consistent. - Passing Context: Server function middleware is not just for protection; it can also enrich the function's context, for example by providing the validated user's ID and roles to your business logic.
Next Up:
We can now reliably determine if a user is logged in on the server. However, our client-side application is still in the dark. How does our UI know whether to show a "Login" or "Logout" button? How do we display the user's name?
In the next lesson, we will address this by learning how to create a typed authentication context for use on both the server and the client, bridging the gap between our server-side security and our client-side user experience.
Can't find a good explanation? Sign up and we'll make it for you
Sign up