Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Server-Side Middleware: Request and Response Handling

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: request and server function.
  • Implement server function middleware to add authentication checks to your createServerFn calls.
  • Pass type-safe context, like a user object, from middleware to your server functions.
  • Apply request middleware 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.

  1. 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.
  2. 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 standard Request and Response objects.

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 createServerFn calls 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.ts for 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.

Can't find a good explanation? Sign up and we'll make it for you

Sign up