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-inlineandunsafe-evalare 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
localecookie for subsequent requests. - i18n context: The request runs inside
runWithI18nLocale, which sets the active locale foruse-intlmessage 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
.mdxare redirected to the clean version. This lets users link to/docs/getting-started.mdxand still reach the correct page. - Markdown negotiation: If the
Acceptheader preferstext/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 Pattern | Route File |
|---|---|
/ | src/routes/(home)/index.tsx |
/en-US/sign-in | src/routes/(auth)/sign-in.tsx |
/en-US/dashboard | src/routes/(dashboard)/index.tsx |
/api/health | src/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
- Architecture Overview — system design and layer breakdown.
- Internal Packages — each monorepo package explained.