MCPInstall

middleware

createMiddleware, request middleware (.server only), server function middleware (.client + .server), context passing via next({ context }), sendContext for client-server transfer, global middleware via createStart in src/start.ts, middleware factories, method order enforcement, fetch override precedence.

Installs
193
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/tanstack/router --skill middleware
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads middleware automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Middleware

Middleware customizes the behavior of server functions and server routes. It is composable — middleware can depend on other middleware to form a chain.

CRITICAL: TypeScript enforces method order: middleware() → validator() → client() → server(). Wrong order causes type errors. CRITICAL: Validating the shape of sendContext (e.g. z.string().uuid().parse(...)) is NOT authorization. A parsed identifier is a well-formed identifier, not an authorized one. Always re-check access against the session principal before using a client-sent ID as a query key, filter, or path parameter.

Two Types of Middleware

FeatureRequest MiddlewareServer Function Middleware
ScopeAll server requests (SSR, routes, functions)Server functions only
Methods.server().client(), .server()
Input validationNoYes (.validator())
Client-side logicNoYes
Created withcreateMiddleware()createMiddleware({ type: 'function' })

Request middleware cannot depend on server function middleware. Server function middleware can depend on both types.

Request Middleware

Runs on ALL server requests (SSR, server routes, server functions):

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware().server(
  async ({ next, context, request }) => {
    console.log('Request:', request.url)
    const result = await next()
    return result
  },
)

Server Function Middleware

Has both client and server phases:

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'

const authMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next }) => {
    // Runs on client BEFORE the RPC call
    const result = await next()
    // Runs on client AFTER the RPC response
    return result
  })
  .server(async ({ next, context }) => {
    // Runs on server BEFORE the handler
    const result = await next()
    // Runs on server AFTER the handler
    return result
  })

Attaching Middleware to Server Functions

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'

const fn = createServerFn()
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    // context contains data from middleware
    return { user: context.user }
  })

Context Passing via next()

Pass context down the middleware chain:

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await getSession(request.headers)
  if (!session) throw new Error('Unauthorized')

  return next({
    context: { session },
  })
})

const roleMiddleware = createMiddleware()
  .middleware([authMiddleware])
  .server(async ({ next, context }) => {
    console.log('Session:', context.session) // typed!
    return next()
  })

Sending Context Between Client and Server

Client → Server (sendContext)

const workspaceMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next, context }) => {
    return next({
      sendContext: {
        workspaceId: context.workspaceId,
      },
    })
  })
  .server(async ({ next, context }) => {
    // workspaceId available here, but VALIDATE IT
    console.log('Workspace:', context.workspaceId)
    return next()
  })

Server → Client (sendContext in server)

const serverTimer = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    return next({
      sendContext: {
        timeFromServer: new Date(),
      },
    })
  },
)

const clientLogger = createMiddleware({ type: 'function' })
  .middleware([serverTimer])
  .client(async ({ next }) => {
    const result = await next()
    console.log('Server time:', result.context.timeFromServer)
    return result
  })

Input Validation in Middleware

import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'

const workspaceMiddleware = createMiddleware({ type: 'function' })
  .validator(zodValidator(z.object({ workspaceId: z.string() })))
  .server(async ({ next, data }) => {
    console.log('Workspace:', data.workspaceId)
    return next()
  })

Global Middleware

Create src/start.ts to configure global middleware:

// src/start.ts
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createStart, createMiddleware } from '@tanstack/react-start'

const requestLogger = createMiddleware().server(async ({ next, request }) => {
  console.log(`${request.method} ${request.url}`)
  return next()
})

const functionAuth = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    // runs for every server function
    return next()
  },
)

export const startInstance = createStart(() => ({
  requestMiddleware: [requestLogger],
  functionMiddleware: [functionAuth],
}))

Using Middleware with Server Routes

All handlers in a route

export const Route = createFileRoute('/api/users')({
  server: {
    middleware: [authMiddleware],
    handlers: {
      GET: async ({ context }) => Response.json(context.user),
      POST: async ({ request }) => {
        /* ... */
      },
    },
  },
})

Specific handlers only

export const Route = createFileRoute('/api/users')({
  server: {
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: async () => Response.json({ public: true }),
        POST: {
          middleware: [authMiddleware],
          handler: async ({ context }) => {
            return Response.json({ user: context.session.user })
          },
        },
      }),
  },
})

Middleware Factories

Create parameterized middleware for reusable patterns like authorization:

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await auth.getSession({ headers: request.headers })
  if (!session) throw new Error('Unauthorized')
  return next({ context: { session } })
})

Attach authMiddleware to every createServerFn that needs auth. Server functions are API endpoints; a route beforeLoad does not protect their data, only the route's UI. Protect the endpoint that reads or mutates private data. See @tanstack/router-core#router-core/auth-and-guards and start-core/auth-server-primitives.

type Permissions = Record<string, string[]>

function authorizationMiddleware(permissions: Permissions) {
  return createMiddleware({ type: 'function' })
    .middleware([authMiddleware])
    .server(async ({ next, context }) => {
      const granted = await auth.hasPermission(context.session, permissions)
      if (!granted) throw new Error('Forbidden')
      return next()
    })
}

// Usage
const getClients = createServerFn()
  .middleware([authorizationMiddleware({ client: ['read'] })])
  .handler(async () => {
    return { message: 'The user can read clients.' }
  })

Custom Headers and Fetch

Setting headers from client middleware

const authMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    return next({
      headers: { Authorization: `Bearer ${getToken()}` },
    })
  },
)

Headers merge across middleware. Later middleware overrides earlier. Call-site headers override all middleware headers.

Custom fetch

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import type { CustomFetch } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const customFetch: CustomFetch = async (url, init) => {
      console.log('Request:', url)
      return fetch(url, init)
    }
    return next({ fetch: customFetch })
  },
)

Fetch precedence (highest to lowest): call site → later middleware → earlier middleware → createStart global → default fetch.

Common Mistakes

1. CRITICAL: Trusting client sendContext — shape check is not access check

sendContext from a client middleware arrives on the server as untrusted client input. Most agents stop after parsing the shape with Zod and assume the value is safe. It isn't: a parsed UUID is some workspace, not the requesting user's workspace. Without a membership check against the session principal, you've built a tenant-walking endpoint.

Layer 1 — WRONG (no validation):

.server(async ({ next, context }) => {
  // SQL-injectable AND tenant-walkable
  await db.query(`SELECT * FROM workspace_${context.workspaceId}`)
  return next()
})

Layer 2 — STILL WRONG (shape only):

.server(async ({ next, context }) => {
  // Looks safe, isn't. UUID is well-formed but the user may not be a member.
  const workspaceId = z.string().uuid().parse(context.workspaceId)
  await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
  return next()
})

Layer 3 — CORRECT (shape AND access):

.middleware([authMiddleware]) // session loaded from cookie, NOT from sendContext
.server(async ({ next, context }) => {
  const workspaceId = z.string().uuid().parse(context.workspaceId)
  // Verify the session principal can access this workspace.
  const member = await db.memberships.find({
    userId: context.session.userId,
    workspaceId,
  })
  if (!member) throw new Error('Not a member of this workspace')
  await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
  return next({ context: { workspaceId } })
})

The session itself must come from a server-trusted source (the cookie + DB lookup in authMiddleware), never from sendContext — anything the client can send, the client can lie about. See start-core/auth-server-primitives.

2. MEDIUM: Confusing request vs server function middleware

Request middleware runs on ALL requests (SSR, routes, functions). Server function middleware runs only for createServerFn calls and has .client() method.

3. HIGH: Browser APIs in .client() crash during SSR

During SSR, .client() callbacks run on the server. Browser-only APIs like localStorage or window will throw ReferenceError:

// WRONG — localStorage doesn't exist on the server during SSR
const middleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const token = localStorage.getItem('token')
    return next({ sendContext: { token } })
  },
)

// CORRECT — use cookies/headers or guard with typeof window check
const middleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const token =
      typeof window !== 'undefined' ? localStorage.getItem('token') : null
    return next({ sendContext: { token } })
  },
)

4. MEDIUM: Wrong method order

// WRONG — type error
createMiddleware({ type: 'function' })
  .server(() => { ... })
  .client(() => { ... })

// CORRECT — middleware → validator → client → server
createMiddleware({ type: 'function' })
  .middleware([dep])
  .validator(schema)
  .client(({ next }) => next())
  .server(({ next }) => next())

Cross-References

  • start-core/server-functions — what middleware wraps
  • start-core/server-routes — middleware on API endpoints
  • start-core/auth-server-primitives — building the authMiddleware factory itself: session cookie reads, OAuth state, CSRF
  • @tanstack/router-core#router-core/auth-and-guards — routing-side UX guards; data auth belongs in the server function, server route, or API endpoint handler/middleware

FAQ

What does the middleware skill do?

createMiddleware, request middleware (.server only), server function middleware (.client + .server), context passing via next({ context }), sendContext for client-server transfer, global middleware via createStart in src/start.ts, middleware factories, method order enforcement, fetch override precedence.

How do I install middleware?

Run npx skills add https://github.com/tanstack/router --skill middleware in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/middleware/ (personal) or .claude/skills/middleware/ (project).

Which AI agents can use middleware?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is middleware safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains middleware?

It is published by tanstack in tanstack/router. It has 193 recorded installs.

More from tanstack/router

View all →

router-core

tanstack/router

Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills.

602 installs

react-router

tanstack/router

React bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. React-specific patterns for hooks, providers, SSR hydration, and createLink with forwardRef.

Frontend & Design559 installs

react-start

tanstack/router

React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook.

Frontend & Design465 installs

start-core

tanstack/router

Open the skill to read its SKILL.md instructions.

453 installs

router-query

tanstack/router

Open the skill to read its SKILL.md instructions.

372 installs

start-server-core

tanstack/router

Open the skill to read its SKILL.md instructions.

Backend & APIs360 installs

Popular backend & apis skills

View category →

find-skills

vercel-labs/skills

Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.

OfficialBackend & APIs4.1M installs

entra-app-registration

microsoft/azure-skills

Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.

OfficialBackend & APIs566.7K installs

This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).

Backend & APIs514.8K installs

embedded-captions

heygen-com/hyperframes

Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.

Backend & APIs453.3K installs

entra-agent-id

microsoft/azure-skills

Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).

OfficialBackend & APIs336.6K installs

neon

neondatabase/agent-skills

Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.

Backend & APIs237.1K installs