Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on migrating React SPAs to SSR with Tanstack Start.
In our last lesson, we delved into the build process behind createServerFn, confirming that server-only logic is securely stripped from the client bundle. This gives us confidence in moving sensitive operations to the server.
However, as applications grow, you'll find that many server functions share common requirements, such as checking for an authenticated user, logging requests, or adding specific headers. Repeating this logic in every function is inefficient and a recipe for bugs.
Today, we'll solve this problem by tackling the learning outcome: Implement server-side middleware for handling requests and responses. Middleware allows you to create reusable, composable pieces of logic that intercept and process requests before they reach their final destination. This is a fundamental pattern for building clean and maintainable server-side code.
By the end of this 60-minute lesson, you will be able to:
- Differentiate between the two types of middleware in Tanstack Start:
requestandserver function. - Implement
server functionmiddleware to add authentication checks to yourcreateServerFncalls. - Pass type-safe context, like a user object, from middleware to your server functions.
- Apply
requestmiddleware to traditional server routes (API endpoints). - Understand how to compose middleware and apply it globally.
Let's get started.
1. The Core Idea: What is Middleware?
If you have experience with backend frameworks like Express.js or Koa.js, the concept of middleware will be familiar. It's a function (or a series of functions) that sits in the middle of the request-response cycle. It can inspect the incoming request, modify it, pass it along to the next function in a chain, and even handle the response itself.
In Tanstack Start, middleware serves this exact purpose, allowing you to centralize logic that applies to multiple endpoints.
Tanstack Start is now my Go-To Framework
To see how this applies directly to Tanstack Start, watch this short clip from the 'Tanstack Start is now my Go-To Framework' video by backpine labs. It perfectly frames the problem and introduces middleware as the solution, especially for authentication.
Starting around three and a half minutes in, watch the middleware section. The video explains how you can extract overlapping logic from server functions into a reusable middleware that checks for authentication and provides context to the functions that use it.
2. Two Types of Middleware: A Critical Distinction
Tanstack Start provides two distinct types of middleware, and choosing the right one depends on what you're applying it to.
- Server Function Middleware: Designed specifically for
createServerFn. It intercepts the RPC-style calls made from the client to your server functions. This is where you'll handle logic like user authentication for mutations or data fetches initiated by client components. - Request Middleware: Designed for Server Routes, which are traditional API endpoints (e.g.,
GET /api/users,POST /api/login). It intercepts raw HTTP requests and has access to the standardRequestandResponseobjects.
The official documentation provides a clear table summarizing their differences.
Middleware | TanStack Start React Docs
Let's consult the official Tanstack Start documentation to solidify our understanding of the two middleware types.
Read the section 'Middleware Types' to review the middleware variants, and pay close attention to the 'Key Differences' table. This table is an excellent reference for deciding which middleware type to use.
The key takeaway is:
- If you're securing a
createServerFn, use server function middleware. - If you're building a classic REST-like API endpoint within your app, use request middleware.
We'll focus first on server function middleware, as it's the most direct extension of our previous lesson.
3. Implementing Server Function Middleware for Authentication
Let's build the most common use case for middleware: ensuring a user is authenticated before allowing a server function to execute.
The "Before" State: Logic Inside the Server Function
Imagine you have a server function to create a post. Without middleware, your authentication logic would live directly inside the handler:
import { createServerFn } from '@tanstack/react-start';
import { validateRequest } from '~/lib/auth'; // A utility to validate session cookie
export const createPost = createServerFn('POST', async (postContent: string) => {
// 1. Boilerplate auth check
const { user } = await validateRequest();
if (!user) {
throw new Error('UNAUTHORIZED');
}
// 2. Core business logic
console.log(`User ${user.id} is creating a post.`);
// ...database logic to create the post...
return { success: true };
});
This works, but you'd have to repeat the auth check in every protected function. Let's refactor this using middleware.
The "After" State: Using an Authentication Middleware
The process involves two steps: creating the middleware, and then applying it.
Step 1: Create the Middleware
A middleware's primary job is to perform its logic and then call next() to pass control to the next item in the chain (which could be another middleware or the final function handler). The real power comes from passing a context object to next().
Learning about Middleware in Tanstack Start
Watching a developer work through this process is incredibly insightful. This video from Web Dev Cody shows the exact process of identifying the need for middleware, creating it, and figuring out how to pass context.
Watch the middleware refactoring. You'll see the developer refactor authentication logic out of a server function and into a new middleware. Pay special attention around 16:30 when he discovers the next({ context: { ... } }) pattern in the docs. This is the 'Aha!' moment and the key to making middleware useful.
As you saw, the key is to create a middleware that validates the request and then passes the user information along in the context. Here is what that authMiddleware would look like:
// src/middleware/auth.ts
import { createMiddleware } from '@tanstack/react-start';
import { validateRequest } from '~/lib/auth';
export const authMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
const { user } = await validateRequest();
if (!user) {
// You can throw an error or return a specific response
throw new Error('UNAUTHORIZED');
}
// Call next(), passing the user object in the context
return next({
context: {
user: user, // This will be available in subsequent middleware and the final handler
},
});
},
);
Step 2: Apply the Middleware
Now, we can simplify our createServerFn by applying the middleware and accessing the user from the type-safe context object.
import { createServerFn } from '@tanstack/react-start';
import { authMiddleware } from '~/middleware/auth'; // Import our new middleware
export const createPost = createServerFn('POST')
.middleware([authMiddleware]) // Apply the middleware
.handler(async (postContent: string, { context }) => {
// No more boilerplate! The user is guaranteed to be here.
// And `context.user` is fully typed!
console.log(`User ${context.user.id} is creating a post.`);
// Core business logic
// ...database logic to create the post...
return { success: true };
});
This approach is cleaner, more secure, and highly reusable. You can now apply authMiddleware to any server function that requires authentication.
4. Implementing Request Middleware for Server Routes
While server functions are great for RPC, sometimes you need a standard API endpoint. For example, you might need a webhook endpoint for a third-party service or a simple GET route to serve a file. This is where Server Routes and Request Middleware come in.
You define a server route by exporting a server object from a route file. This object can contain handlers for different HTTP methods and a middleware array.
Server Routes | TanStack Start React Docs
The documentation on Server Routes explains how to define these endpoints and apply middleware to them.
Read the sections 'Defining a Server Route' to review route configuration, 'Adding middleware to all handlers' for route-level middleware, and 'Combining route-level and handler-specific middleware' for mixed middleware approaches. This shows the syntax for attaching request middleware to the server property of a route definition.
Here's an example of a server route with a logging middleware.
1. Create a Request Middleware:
Note that we don't specify type: 'function', as 'request' is the default.
// src/middleware/logger.ts
import { createMiddleware } from '@tanstack/react-start';
export const loggerMiddleware = createMiddleware().server(async ({ request, next }) => {
console.log(`--> ${request.method} ${request.url}`);
const response = await next(); // Continue to the handler
console.log(`<-- ${response.status}`);
return response;
});
2. Define the Server Route and Apply Middleware:
// src/routes/api/health.ts
import { createFileRoute } from '@tanstack/react-router';
import { json } from '@tanstack/react-start';
import { loggerMiddleware } from '~/middleware/logger';
export const Route = createFileRoute('/api/health')({
server: {
middleware: [loggerMiddleware], // Applies to all handlers in this route
handlers: {
GET: () => {
return json({ status: 'ok', timestamp: new Date().toISOString() });
},
},
},
});
When you make a GET request to /api/health, your server console will show the log messages from the loggerMiddleware before and after the GET handler runs.
5. Global Middleware
For concerns that should apply to every single request or server function—like comprehensive logging or performance monitoring—you can define global middleware. This is configured once in your src/start.ts (or src/start.tsx) file.
// src/start.tsx
import { createStart } from '@tanstack/react-start'
import { myGlobalLoggingMiddleware } from './middleware'
import { myGlobalAuthMiddleware } from './middleware'
export const start = createStart((opts) => {
return {
// This runs for EVERY server route and SSR request
requestMiddleware: [myGlobalLoggingMiddleware],
// This runs for EVERY server function
functionMiddleware: [myGlobalAuthMiddleware],
}
})
This is a powerful feature for enforcing application-wide policies. The documentation section on Global Middleware provides further details, including an important note on ensuring type safety when using global middleware.
Conclusion
Today we've seen how middleware in Tanstack Start provides a powerful, clean way to handle cross-cutting concerns. By intercepting requests, you can abstract away common logic like authentication, logging, and validation, making your primary business logic cleaner and more focused.
Key Takeaways:
- Middleware intercepts requests to add reusable logic.
- Server Function Middleware is for securing
createServerFncalls and uses the{ type: 'function' }option. - Request Middleware is for traditional API-style Server Routes.
- The
next({ context: { ... } })pattern is the mechanism for passing type-safe data from a middleware to the final handler. - Middleware can be composed and applied globally in
start.tsfor application-wide concerns.
In our next lesson, we will zoom out from the application code and look at the environment it runs in. We will compare the development and runtime characteristics of Node.js vs. Bun for a Tanstack Start application, exploring the trade-offs in performance, tooling, and compatibility.