Correct usage of middlewares #7636
|
Hi Tanner and other mainteners! ❤️ I'm new here. Beforehand, thank you for the great product, i love all stack tools ❤️ I came from long history of AngularJS -> NextJS (which i hate now for doing crazy hardcoded stuff) -> Tanstack start I have few questions about middlewares and specifically usage in page/api routes and server functions. Let's first discuss the simple example use case. I have unauthenticated page, authenticated page. Authenticated page needs to check authentication, attach some data to context (cookie valid, user session valid) and pass it (so other middleware and handler can access it). So for backend function i chose server functions, where loader call server function directly and UI (page logic) will call server function (if needed) via ( Let's write simple server function // src/server/function/getTestData.function.ts
import { createServerFn } from '@tanstack/react-start';
import { z } from 'zod';
interface Result {
serverTime: number;
}
export const getTestData = createServerFn({ method: 'GET' })
.handler(({ data: { note } }) => {
return new Promise<Result>((resolve) =>
setTimeout(() => resolve({ serverTime: Date.now() }), 200),
);
});// src/app/example/index.tsx
import { useQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';
import { useServerFn } from '@tanstack/react-start';
import { getTestData } from '@/server/functions/getTestData.function';
export const Route = createFileRoute('/example')({
component: TestComponent,
loader: () => getTestData(),
});
function TestComponent() {
const { serverTime } = Route.useLoaderData();
const getTestDataFn = useServerFn(getTestData);
const { refetch, data } = useQuery({ queryFn: getTestDataFn, queryKey: 'test-data', enabled: false });
return (
<div>
<h1>Test</h1>
<p>Server time from SSR loader: </p>
{serverTime}
{data && (
<>
<p>Data:</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
)}
<button type="button" onClick={() => refetch()}>
Load data again from UI
</button>
</div>
);
}Now is expected, that authenticated route and it's server functions will check the user and attach it somewhere to be accessed. // src/server/lib/checkSession.ts
async function checkSession(sessionId: string): { userId: number; } | null {
const user = checkSessionInDB(sessionId);
return user ?? null;
}// src/server/middlewares/withAuth.ts
import { AsyncLocalStorage } from 'node:async_hooks';
import { getCookie } from '@tanstack/react-start/server';
const storage = new AsyncLocalStorage<RequestContext>();
function _getStoreInternally(): RequestContext {
const store = storage.getStore();
if (!store) {
throw new Error('Request data ALS store not found, ensure function is called within request context');
}
return store;
}
export function getStore(): DeepReadonly<RequestContext> {
return _getStoreInternally();
}
export interface RequestContext {
session: { id: number; } | null;
}
export const withAuth = createMiddleware().server(async (async { next }) => {
const authCookie = getCookie("auth-cookie");
if (!authCookie) {
return new Response(null, {
headers: { Location: 'https://host/unauthenticated' },
status: 307,
})
}
const user = await checkSession(authCookie);
if (!user) {
return new Response(null, {
headers: { Location: 'https://host/unauthenticated' },
status: 307,
})
}
// I'm used to attach nodejs AsyncLocalStorage to be accessible in same request context
return getStorage().run({ userId: user.id }, async () => await next());
});So, we have middleware now, i assume we attach it to route definition export const Route = createFileRoute('/example')({
server: {
midddleware: [withAuth]
},
component: TestComponent,
loader: () => getTestData(),
});Question 1Ok that works, but what if this page needs to load data in loader with our server function? If i don't put same middleware in server function, i still can use that store inside server function, export const getTestData = createServerFn({ method: 'GET' })
.handler(({ data: { note } }) => {
// <------- If this function is called from router `loader` and i access `getStore()` here, i'll get that store with user
return new Promise<Result>((resolve) =>
setTimeout(() => resolve({ serverTime: Date.now() }), 200),
);
});But, ofc when i call this function directly, there is no store, because page middleware will not run and this route fn has no middleware. Same issue when there is hot reload So what's the correct solution here? If i'll put same middleware in server fn, it will fix direct call, but that same middleware will be called twice when acessing page? (page middleware, loader -> server fn middleware) Question 2I though when using I fixed it with using Cannot find out in docs. It works and browser don't complains anymore.
Question 3With this knowledge of middlewares doing stuff and returing data. What about when i want to check user before page load, load some data in loader and also returning that user into page itself as data. I can ofcourse use Can you suggest some better abstraction alternative, how to do this? Maybe on root level (layout? if page should be authenticated). Some examples would be great. I hope it wasn't too complicated. Thanks in advance. John |
Replies: 1 comment 2 replies
|
My mental model would be: route middleware / beforeLoad protects the route UX, but server functions still need to protect themselves because they are callable independently from the route that originally rendered the page. So for question 1, I would not rely on the route middleware context being present inside the server function. It may work when the server function is called from the loader in the same request path, but it breaks as soon as the server function is called directly from the client, during HMR, or from another route. If getTestData reads private data, I would attach the auth middleware to getTestData as well. Yes, that can mean the auth check runs twice during the initial page load: once for the route and once for the server function. But that is usually better than having a server function that is only safe when called from one specific route. If the double check becomes expensive, I would try to make the session lookup cheap/cacheable per request rather than removing the auth check from the server function. For question 2, this matches my understanding of TanStack Start’s execution model: code is isomorphic by default unless you explicitly keep it server-only. So Node-only things like node:async_hooks, database clients, filesystem access, etc. should be behind a server-only boundary or in files that are only imported from server code. Using createServerOnlyFn for the ALS/storage accessor sounds like the right direction if it prevents the Node module from leaking into the client bundle. For question 3, I would put shared auth/user loading at a layout/root route boundary instead of repeating it in every page. For example, have an authenticated route group/layout that checks the session and returns the user/session in its loader/context, then child routes can read it from the parent route context or loader data. Individual server functions should still use the same auth primitive/middleware when they access private data, but the UI does not need to repeat the same user loader everywhere. So the split I would aim for is:
That keeps the security model explicit while still avoiding duplicated auth logic across every page. |
My mental model would be: route middleware / beforeLoad protects the route UX, but server functions still need to protect themselves because they are callable independently from the route that originally rendered the page.
So for question 1, I would not rely on the route middleware context being present inside the server function. It may work when the server function is called from the loader in the same request path, but it breaks as soon as the server function is called directly from the client, during HMR, or from another route. If getTestData reads private data, I would attach the auth middleware to getTestData as well.
Yes, that can mean the auth check runs twice during the initial p…