mit einem Klick
integration-patterns
Complete flow examples combining TanStack Form, Query, Router, and Start. Use when implementing end-to-end features that span multiple systems.
Menü
Complete flow examples combining TanStack Form, Query, Router, and Start. Use when implementing end-to-end features that span multiple systems.
Git workflow for branches, commits, and PRs. Use for commit, branch, pr, pull request, conventional, push, feat, fix, chore, merge, rebase
Storybook stories and interaction tests. Use for story, stories, storybook, chromatic, visual test, interaction test, play function, component test
TypeScript and unicorn linting patterns. Use for typescript, array, for-of, reduce, forEach, throw, catch, modern js, es modules, string, number, error handling
Zod v4 schema validation. Use for zod, schema, validation, parse, safeParse, infer, coerce, transform, refine, z.object, z.string, z.email, z.url
Better Auth authentication. Use for auth, login, logout, session, user, signup, register, protect, middleware, password, oauth, social
Drizzle ORM + PostgreSQL database layer. Use for db, database, query, schema, table, migrate, sql, postgres, drizzle, model, relation
| name | integration-patterns |
| description | Complete flow examples combining TanStack Form, Query, Router, and Start. Use when implementing end-to-end features that span multiple systems. |
Quick reference for common full-stack flows. Each flow has a dedicated file with complete copy-paste examples.
| Flow | Components | Use Case |
|---|---|---|
| Suspense Query + Loader | useSuspenseQuery, Router, Loader | SSR-ready data fetching |
| Form → Server → Query | Form, Server Function, Query | Create/update resources |
| Infinite List | Infinite Query, Server Function | Paginated feeds, timelines |
| Paginated Table | Table, Query, Router Search | Admin dashboards, data grids |
| Auth → Protected Route | Auth Client, Middleware, Router | Login, session, guards |
| Error Handling | Error Boundaries, Toast | Error recovery, user feedback |
Custom hooks should be placed based on their scope:
| Location | When to Use | Example |
|---|---|---|
packages/*/src/hooks/ | Shared across apps, tied to package | @oakoss/auth session hooks |
apps/web/src/modules/*/hooks/ | Module-specific, reused within module | usePostFilters in posts |
apps/web/src/hooks/ | App-wide, used by multiple modules | useAppForm, useToast |
# Package hooks - exported from package
packages/auth/src/hooks/use-session.ts # @oakoss/auth/hooks
# Module hooks - domain-specific
apps/web/src/modules/posts/hooks/use-posts-query.ts
apps/web/src/modules/users/hooks/use-user-options.ts
# Global app hooks - app-wide utilities
apps/web/src/hooks/use-app-form.ts
apps/web/src/hooks/form-context.ts
Decision tree:
packages/*/src/hooks/apps/web/src/modules/*/hooks/apps/web/src/hooks/The preferred pattern for SSR-ready data fetching: use useSuspenseQuery with route loaders to ensure data is ready before render.
Key pieces:
// 1. Query options hook (apps/web/src/modules/posts/hooks/use-posts-options.ts)
import { queryOptions } from '@tanstack/react-query';
import { getPosts } from '../server/get-posts';
export function postsOptions() {
return queryOptions({
queryKey: ['posts'],
queryFn: () => getPosts(),
staleTime: 1000 * 60, // 1 minute
});
}
export function postOptions(id: string) {
return queryOptions({
queryKey: ['posts', id],
queryFn: () => getPost({ data: { id } }),
staleTime: 1000 * 60,
});
}
// 2. Route with loader ensures data is cached before render
export const Route = createFileRoute('/_app/posts')({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(postsOptions());
},
component: PostsPage,
});
// 3. Component uses useSuspenseQuery - data is guaranteed
import { useSuspenseQuery } from '@tanstack/react-query';
function PostsPage() {
const { data: posts } = useSuspenseQuery(postsOptions());
// posts is always defined - no loading state needed here
return <PostList posts={posts} />;
}
With route params:
// Route with dynamic param
export const Route = createFileRoute('/_app/posts/$id')({
loader: async ({ context, params }) => {
await context.queryClient.ensureQueryData(postOptions(params.id));
},
component: PostPage,
});
function PostPage() {
const { id } = Route.useParams();
const { data: post } = useSuspenseQuery(postOptions(id));
return <PostDetail post={post} />;
}
With beforeLoad for auth + data:
export const Route = createFileRoute('/_app/dashboard')({
beforeLoad: async ({ context }) => {
const session = await auth.api.getSession({
headers: context.request.headers,
});
if (!session) throw redirect({ to: '/login' });
return { user: session.user };
},
loader: async ({ context }) => {
// User is guaranteed to exist after beforeLoad
await context.queryClient.ensureQueryData(dashboardOptions());
},
component: DashboardPage,
});
function DashboardPage() {
const { user } = Route.useRouteContext();
const { data } = useSuspenseQuery(dashboardOptions());
return <Dashboard user={user} data={data} />;
}
Pattern summary:
| Step | Purpose | Location |
|---|---|---|
beforeLoad | Auth guards, redirect, inject context | Route definition |
loader | Ensure query data is cached (SSR-ready) | Route definition |
| Query options | Define queryKey, queryFn, staleTime | Module hooks folder |
| Component | Use useSuspenseQuery with same options | Route component |
Creates a resource with validation, server mutation, and cache invalidation.
Key pieces:
import { createServerFn } from '@tanstack/react-start';
import { db } from '@oakoss/database';
import { auth } from '@oakoss/auth/server';
// 1. Server function with auth + validation
export const createPost = createServerFn({ method: 'POST' })
.inputValidator(createPostSchema)
.handler(async ({ data, request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) return { error: 'Unauthorized', code: 'AUTH_REQUIRED' };
// ... insert and return
});
// 2. Form with mutation
const mutation = useMutation({
mutationFn: (values) => createPost({ data: values }),
onSuccess: (result) => {
if (result.success) {
queryClient.invalidateQueries({ queryKey: ['posts'] });
toast.success('Created!');
}
},
});
// 3. Handle server errors in form
if (result.error) {
form.setErrorMap({ onServer: result.error });
}
Cursor-based pagination with intersection observer auto-loading.
Key pieces:
import { createServerFn } from '@tanstack/react-start';
import { db, lt } from '@oakoss/database';
import { posts } from '@oakoss/database/schema';
// 1. Server function returns { items, nextCursor }
export const getPostsInfinite = createServerFn({ method: 'GET' })
.inputValidator(
z.object({ cursor: z.string().optional(), limit: z.number() }),
)
.handler(async ({ data }) => {
const items = await db.query.posts.findMany({
where: data.cursor
? lt(posts.createdAt, new Date(data.cursor))
: undefined,
limit: data.limit + 1,
});
const hasMore = items.length > data.limit;
return {
items: hasMore ? items.slice(0, -1) : items,
nextCursor: hasMore ? items.at(-1)?.createdAt.toISOString() : undefined,
};
});
// 2. Infinite query options
export function postsInfiniteOptions() {
return {
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam }) =>
getPostsInfinite({ data: { cursor: pageParam } }),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
};
}
// 3. Auto-fetch on scroll
const { ref, inView } = useInView();
useEffect(() => {
if (inView && hasNextPage) fetchNextPage();
}, [inView, hasNextPage]);
Server-side pagination with URL state synchronization.
Key pieces:
import { zodValidator } from '@tanstack/zod-adapter';
// 1. Route validates search params
export const Route = createFileRoute('/_app/admin/users')({
validateSearch: zodValidator(
z.object({
page: z.number().default(1),
size: z.number().default(10),
sort: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
}),
),
loaderDeps: ({ search }) => search,
loader: ({ context, deps }) =>
context.queryClient.ensureQueryData(usersQueryOptions(deps)),
});
// 2. Update URL on table state change
const handlePaginationChange = (pagination: PaginationState) => {
navigate({
search: (prev) => ({
...prev,
page: pagination.pageIndex + 1,
size: pagination.pageSize,
}),
});
};
// 3. Server function returns { items, meta: { total, totalPages } }
Login flow with session and route protection.
Key pieces:
import { authClient } from '@oakoss/auth/client';
import { auth } from '@oakoss/auth/server';
import { createMiddleware } from '@tanstack/react-start';
// 1. Login with Better Auth client
const result = await authClient.signIn.email({ email, password });
if (result.error) form.setErrorMap({ onServer: result.error.message });
// 2. Auth middleware
export const authMiddleware = createMiddleware().server(
async ({ request, next }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) throw redirect({ to: '/login' });
return next({ context: { session } });
},
);
// 3. Protected layout applies middleware
export const Route = createFileRoute('/_app')({
server: { middleware: [authMiddleware] },
component: AppLayout,
});
// 4. Access session in components
const { session } = Route.useRouteContext();
Structured errors with boundaries and recovery.
Key pieces:
import { Button } from '@oakoss/ui';
// 1. Return structured errors from server
return { error: 'Not found', code: 'NOT_FOUND' };
// 2. Handle in mutation onSuccess
if ('error' in result) {
switch (result.code) {
case 'AUTH_REQUIRED':
navigate({ to: '/login' });
break;
case 'VALIDATION_ERROR':
form.setFieldMeta(...);
break;
default:
toast.error(result.error);
}
}
// 3. Route error boundaries
export const Route = createFileRoute('...')({
errorComponent: ({ error, reset }) => (
<div>
<p>{error.message}</p>
<Button onPress={reset}>Try Again</Button>
</div>
),
notFoundComponent: () => <NotFoundMessage />,
});
| Mistake | Correct Pattern |
|---|---|
Using useQuery without loader | Use useSuspenseQuery + ensureQueryData in loader for SSR |
Checking isPending in Suspense components | useSuspenseQuery guarantees data - no pending state |
| Hooks in wrong location | Package hooks → module hooks → global hooks (see placement guide) |
| Duplicating query options | Create options hook once, reuse in loader and component |
| Not invalidating cache after mutation | Use queryClient.invalidateQueries({ queryKey }) in onSuccess |
| Missing auth check in server function | Always verify session from request.headers |
| Form not showing server errors | Use form.setErrorMap({ onServer: error }) |
| Infinite query without proper cursor | Provide initialPageParam and getNextPageParam |
| Not prefetching for SSR | Use ensureQueryData in route loaders |
| Table state not synced to URL | Use validateSearch + navigate on change |
| Handling error in onError instead of checking result | Server functions return errors in result, not thrown |
| Not resetting page on filter change | Set page: 1 when search/filter changes |
| Missing loading states | Show skeletons during isPending, overlays during isFetching |
| No error boundary on routes | Add errorComponent and notFoundComponent |
Explore agentcode-reviewer agentsecurity-auditor agent| Skill | Use For |
|---|---|
| tanstack-query | Query patterns, caching, mutations, infinite queries |
| tanstack-form | Form validation, field components, composable forms |
| tanstack-router | Route guards, loaders, search params, navigation |
| tanstack-start | Server functions, API routes, middleware |
| server-functions | createServerFn patterns, validation, auth |
| error-boundaries | Route errors, global errors, recovery |
| auth | Better Auth, sessions, protected routes |
| database | Drizzle ORM, queries, relations |