Raypx

Request Lifecycle

How a request flows through middleware, routing, and API handlers.

Every HTTP request to Raypx passes through a three-stage middleware chain before reaching a route handler. This page traces the full lifecycle from the incoming request to the response.

Overview

Incoming Request
       |
       v
+------------------+     +------------------+     +------------------+
| securityMiddleware| --> | i18nMiddleware   | --> | llmMiddleware    |
| CSP, HSTS,       |     | locale detection |     | MDX suffix,      |
| X-Frame-Options  |     | cookie handling  |     | markdown         |
+------------------+     +------------------+     | negotiation      |
       |                       |                 +------------------+
       |                       |                        |
       v                       v                        v
  Route Matching  <---  URL Rewrite  <---  Path Rewrite
       |
       v
+------------------+     +------------------+
| Server Function   |     | API Handler      |
| (direct call)    | or  | /api/auth/$      |
|                  |     | /api/rpc/$       |
+------------------+     +------------------+
       |
       v
  Response (with security headers attached)

Stage 1: Security Middleware

The first middleware runs last in the response chain but first in setup. It wraps the next() call and attaches security headers to the outgoing response:

// src/middleware/security.ts
const securityHeaders = {
  "X-Content-Type-Options": "nosniff",
  "X-Frame-Options": "DENY",
  "Referrer-Policy": "strict-origin-when-cross-origin",
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
  "X-XSS-Protection": "0",
} as const

export const securityMiddleware = createMiddleware().server(
  async ({ next, request }) => {
    const response = await next()
    if (response instanceof Response) {
      for (const [key, value] of Object.entries(securityHeaders)) {
        response.headers.set(key, value)
      }
      if (process.env.NODE_ENV === "production") {
        response.headers.set(
          "Strict-Transport-Security",
          "max-age=63072000; includeSubDomains; preload",
        )
      }
      // Content-Security-Policy is set with stricter rules in production
      const csp = buildCSP(request.url, process.env.NODE_ENV === "production")
      response.headers.set("Content-Security-Policy", csp)
    }
    return response
  },
)

Key behaviors:

  • X-Frame-Options: DENY prevents clickjacking by disallowing the page from being embedded in iframes.
  • CSP restricts script sources to self and the origin. In development, unsafe-inline and unsafe-eval are allowed for Vite HMR.
  • HSTS is only set in production to avoid issues during local development with HTTP.

Stage 2: i18n Middleware

The internationalization middleware detects the user's locale and ensures the URL has the correct locale prefix:

// src/start.ts (i18nMiddleware)
const i18nMiddleware = createMiddleware().server(
  async ({ next, request }) => {
    const url = new URL(request.url)
    // Skip for server function requests (TanStack Start internal)
    if (url.pathname.startsWith("/_serverFn")) {
      return next()
    }

    const result = handleLocaleMiddleware(request)
    if (result.redirect) {
      return redirect(new URL(result.redirect, request.url))
    }

    const response = await runWithI18nLocale(result.locale, () => next())
    if (result.setCookie && response instanceof Response) {
      response.headers.append("Set-Cookie", createLocaleCookieHeader(result.locale))
    }
    return response
  },
)

Key behaviors:

  • Server function bypass: Requests to /_serverFn (TanStack Start's internal endpoint for server functions) skip locale processing to avoid redirect loops.
  • Locale detection: Reads the locale from the URL path prefix (/en-US/... or /zh-CN/...). If no prefix is found, it redirects to the default locale.
  • Cookie setting: When a user visits a locale-prefixed URL without a cookie, the middleware sets a locale cookie for subsequent requests.
  • i18n context: The request runs inside runWithI18nLocale, which sets the active locale for use-intl message resolution on the server.

Stage 3: LLM/Docs Middleware

The third middleware handles documentation-specific URL rewriting for search engine crawlers and MDX viewers:

// src/start.ts (llmMiddleware)
const llmMiddleware = createMiddleware().server(({ next, request }) => {
  const url = new URL(request.url)
  if (url.pathname.startsWith("/_serverFn")) {
    return next()
  }

  // Rewrite /docs/foo/bar.mdx -> /docs/foo/bar
  const path = rewriteSuffix(url.pathname)
  if (path) {
    throw redirect(new URL(path, url))
  }

  // If the client prefers markdown (e.g., an LLM crawler), serve raw MDX
  if (isMarkdownPreferred(request)) {
    const docsPath = rewriteDocs(url.pathname)
    if (docsPath) {
      throw redirect(new URL(docsPath, url))
    }
  }

  return next()
})

Key behaviors:

  • MDX suffix stripping: URLs ending in .mdx are redirected to the clean version. This lets users link to /docs/getting-started.mdx and still reach the correct page.
  • Markdown negotiation: If the Accept header prefers text/markdown (common for LLM crawlers), the middleware redirects to the raw content endpoint. This makes the documentation crawlable by AI tools.

Route Matching

After all middleware completes, TanStack Start matches the URL to a file in src/routes/:

URL PatternRoute File
/src/routes/(home)/index.tsx
/en-US/sign-insrc/routes/(auth)/sign-in.tsx
/en-US/dashboardsrc/routes/(dashboard)/index.tsx
/api/healthsrc/routes/api/health.ts
/api/rpc/*src/routes/api/rpc/$.ts
/api/auth/*src/routes/api/auth/$.ts

Parenthesized directories like (auth) and (dashboard) are layout groups — they contribute a layout component but do not appear in the URL path.

API Requests

API requests follow a different path depending on the endpoint:

Health Check (/api/health)

A simple Nitro API route that returns { status: "ok" } and checks database connectivity.

oRPC (/api/rpc)

The catch-all route src/routes/api/rpc/$.ts delegates to the oRPC handler. The handler validates input with Zod, executes the matched procedure, and returns the result as JSON. Authentication is handled inside the oRPC context factory, which reads the session from the request headers.

Better Auth (/api/auth)

The catch-all route src/routes/api/auth/$.ts delegates all requests to the Better Auth handler, which manages sign-in, sign-up, OAuth callbacks, session management, and password reset flows.

Server Functions

TanStack Start server functions bypass the HTTP layer entirely during SSR. When a component calls a server function on the server, the function executes in the same process without a network round-trip. On the client, server functions are serialized and sent to /_serverFn via POST. Both the i18n and LLM middleware skip /_serverFn requests to avoid unnecessary processing.

Next Steps

On this page