Hello! Welcome to your seventh lesson in the course on mastering TanStack Start and Router.
In our last session, we focused on structuring our application's UI by implementing nested routes and shared layouts. You learned how to use file-system conventions and the <Outlet /> component to create scalable and maintainable user interfaces.
Lesson 7: Server vs. Client Code Execution
Today's Goal:
This lesson directly addresses the learning outcome: Analyze TanStack Start's mechanism for distinguishing server-only ('use server') and client-side code execution.
Now that we know how to structure the components of our application, we need to understand where the logic associated with those components runs. In an SSR framework like TanStack Start, code can execute in two distinct environments: the Node.js server and the user's browser (the client). Misunderstanding this boundary is a common source of bugs, security vulnerabilities, and performance issues. Your experience as a lead developer will make the architectural implications of these concepts particularly relevant.
What we will cover:
- The "Isomorphic by Default" Principle: TanStack's foundational execution model.
- TanStack's Philosophy: A "client-first" approach to SSR.
- Execution Control APIs: A deep dive into the functions (
createServerFn,createClientOnlyFn, etc.) that give you explicit control over where your code runs. - Route-Level Control: Using the
ssroption to manage execution context for entire routes. - Common Patterns and Anti-Patterns: Practical guidance for writing secure and correct SSR code.
1. The Core Principle: Isomorphic by Default
The most important concept to grasp in TanStack Start's execution model is that all code is isomorphic by default. This means that unless you explicitly state otherwise, your code is included in both the server and client JavaScript bundles and is expected to run in both environments.
A prime example of this is the route loader function, which we'll explore in detail in the next lesson.
- On the initial page load, the
loaderruns on the server to fetch data for Server-Side Rendering. - On subsequent client-side navigations (e.g., clicking a
<Link>), the sameloaderfunction runs in the browser.
This is a critical distinction from some other frameworks where data-loading functions are strictly server-only.
Execution Model | TanStack Start React Docs
Let's start with the official documentation which clearly states this core principle. Understanding this is the key to everything that follows.
Read the sections 'Core Principle: Isomorphic by Default' and 'The Execution Boundary' about execution environments. Pay close attention to the comment in the code example for the route loader: // This runs on server during SSR AND on client during navigation.
2. TanStack's "Client-First" Philosophy
TanStack Start's design philosophy differs from other popular meta-frameworks like Next.js. While Next.js adopts a "server-first" model where components are server components by default and you opt into client-side interactivity with a 'use client' directive, TanStack Start does the opposite.
It is fundamentally a client-side framework that you opt-in to server-side capabilities. This gives you fine-grained control and can feel more familiar if you're coming from a Single-Page Application (SPA) background.
Next Gen Fullstack React with TanStack
To get insight into this philosophy, let's hear from TanStack's creator, Tanner Lindsley. In this clip from the Syntax.fm podcast, he explains the 'client-side first' approach.
Watch the framework comparison. Listen for the distinction he makes between TanStack Router being 'client side out of the gate' and Next.js being 'server first'.
This "opt-in" model is realized through a set of explicit APIs that we'll explore next.
3. APIs for Explicit Execution Control
TanStack Start provides a suite of functions to precisely control where your code executes. These functions are the primary mechanism for separating server and client logic.
The video below gives a quick, practical overview of these functions before we dive into the specifics with the official documentation.
Tanstack Start is now my Go-To Framework
This short clip from backpine labs provides a great visual introduction to the different types of functions available for managing execution context.
Watch the brief section on managed functions. The video quickly introduces isomorphic, server-only, and client-only functions, which we will now examine in detail.
Now, let's use the documentation to understand each API's purpose and syntax.
Execution Model | TanStack Start React Docs
This is the most critical part of the lesson. The official documentation provides detailed explanations and code examples for each of the execution control APIs.
Read the sections 'Execution Control APIs' and 'Environment-Specific Implementations'. As you read, focus on the table that summarizes the APIs and their behavior. We'll break them down below.
Let's summarize what you just read into a clear decision framework.
| API | Use Case | When called on Client... | When called on Server... |
|---|---|---|---|
createServerFn() | RPC calls (e.g., mutations, secure data queries). | Makes a network request. | Executes the function directly. |
createServerOnlyFn() | Server-only utilities (e.g., accessing secrets, DB clients). | Throws an error. | Executes the function directly. |
createClientOnlyFn() | Client-only utilities (e.g., accessing localStorage). | Executes the function directly. | Throws an error. |
createIsomorphicFn() | Functions needing different logic for server vs. client environments. | Executes the .client() logic. | Executes the .server() logic. |
<ClientOnly> | Components that need browser APIs (e.g., charting libraries). | Renders children. | Renders a fallback. |
A Note on createServerFn vs. 'use server'
You might have seen the 'use server' directive in React. While TanStack Start supports it, the recommended approach is to use createServerFn. It's more explicit, type-safe, and configurable (e.g., defining HTTP methods, middleware).
Tanner Lindsley explains his rationale for this design choice:
Next Gen Fullstack React with TanStack
In this segment, Tanner Lindsley critiques the 'use server' directive and explains why he built the more powerful createServerFn API as an alternative.
Watch the createServerFn overview. He discusses the 'magic' of 'use server' and how createServerFn provides more control and type safety, which is especially important in complex or enterprise applications.
4. Route-Level Control: Selective SSR
Besides function-level control, you can also manage execution at the route level using the ssr option in your route definitions. This is useful when an entire page or section of your app has specific rendering requirements.
Selective Server-Side Rendering (SSR)
The 'Selective SSR' documentation explains how to control server-side execution for entire routes. This is a powerful feature for optimizing your application.
Read the sections for ssr: true, ssr: false, and ssr: 'data-only'. Understand the difference between them in terms of where the loader runs and where the component renders.
Here's a summary of the ssr options:
ssr: true(Default): Full SSR. Theloaderruns on the server, and the component is rendered to HTML on the server for the initial load.ssr: false: No SSR (SPA mode for this route). Theloaderand component rendering are both deferred to the client. This is useful for routes that are heavy on client-side APIs, like a dashboard with many charts.ssr: 'data-only': Hybrid mode. Theloaderruns on the server, and the data is sent to the client. However, the component itself is rendered only on the client. This is a great middle-ground when your component needs browser APIs, but the initial data can be fetched securely and quickly on the server.
5. Common Patterns and Anti-Patterns
Understanding the theory is one thing; applying it correctly is another. Let's look at a common mistake and how to fix it using the APIs we've learned.
Execution Model | TanStack Start React Docs
This final reading from the 'Execution Model' docs covers common mistakes developers make. The 'Incorrect Loader Assumptions' example is particularly important.
Read the section 'Common Anti-Patterns' and focus on the code example showing why you should not access process.env directly in a loader and how to fix it by wrapping the logic in a createServerFn. Then, read the 'Architecture Decision Framework' section to review the environment guidelines.
The Key Anti-Pattern: Server Secrets in Isomorphic Code
❌ Wrong: Accessing a server secret directly in a route loader.
// ❌ This code is ISOMORPHIC.
// `process.env.SECRET` will be undefined on the client, and worse,
// the bundler might accidentally expose the variable to the client bundle.
export const Route = createFileRoute('/users')({
loader: async () => {
const secret = process.env.SECRET_API_KEY; // DANGER
const res = await fetch(`https://api.example.com/users?key=${secret}`);
return res.json();
},
});
✅ Right: Wrapping the server-only logic in a createServerFn.
import { createServerFn } from '@tanstack/react-start';
// ✅ This function is defined once and is guaranteed to only run on the server.
const getUsersSecurely = createServerFn('GET', async () => {
// This code is now safely on the server.
const secret = process.env.SECRET_API_KEY;
const res = await fetch(`https://api.example.com/users?key=${secret}`);
return res.json();
});
// ✅ The loader remains isomorphic, but it now makes a safe RPC call.
export const Route = createFileRoute('/users')({
loader: async () => {
return getUsersSecurely(); // This is a network call when run on the client.
},
});
This pattern is fundamental to building secure applications with TanStack Start.
Conclusion
Today, you've analyzed the core mechanisms TanStack Start uses to manage code execution across server and client environments. This knowledge is crucial for writing performant, secure, and bug-free SSR applications.
Key Takeaways:
- Isomorphic by Default: Assume code runs everywhere unless you specify otherwise. Route
loaders are a key example. - Explicit Control is Key: Use the provided APIs (
createServerFn,createClientOnlyFn, etc.) to clearly define where your logic should execute. - Protect Your Secrets: Never access sensitive environment variables, database clients, or file systems in isomorphic code. Always wrap such logic in a
createServerFnorcreateServerOnlyFn. - Use the Right Tool:
- Use
createServerFnfor server logic you need to call from the client (RPC). - Use
ssr: falseor<ClientOnly>for components that are fundamentally browser-only. - Use
ssr: 'data-only'for a smart hybrid approach.
- Use
Next Lesson Preview:
In our next lesson, we will build directly on today's concepts by focusing on "Implement server-side data fetching in route loaders with typed data returns." You've learned how to create secure server functions; next, you'll learn how to integrate them into your routes to fetch data and ensure that the data is fully type-safe from the server all the way to your components.
Can't find a good explanation? Sign up and we'll make it for you
Sign up