Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on transforming SPAs into modern SSR applications.
In our last lesson, we built a more resilient application by implementing route-level error boundaries and pending UI states. This allows us to gracefully handle slow or failed data-fetching operations.
Today, we'll shift our focus from reacting to errors to proactively preventing them. We'll tackle a common source of bugs and fragile code: user-provided data in the URL. Our learning outcome is to access and validate URL search parameters in route loaders using a schema library like Zod.
This is a critical skill for building robust, shareable SSR pages. By validating search parameters (e.g., for pagination, sorting, or filtering), we ensure our loaders only receive clean, predictable data, making our server-side logic simpler and safer.
This lesson will cover:
- Why traditional URL search parameter handling falls short in complex applications.
- How Tanstack Router elevates search params to a first-class form of application state.
- Using the
validateSearchroute option with Zod to create a single source of truth for validation. - Accessing the type-safe, validated search parameters within your server-side
loaderfunctions.
1. Search Params as Application State
As a seasoned front-end developer, you've likely spent a lot of time managing application state. In many SPAs, state related to filters, sorting, or pagination lives in React state (useState, Redux, etc.). For an SSR application, however, it's crucial to store this state in the URL so that pages are shareable, bookmarkable, and refreshable.
The problem is that native browser APIs for search parameters (URLSearchParams) are primitive. They treat everything as strings and have no concept of data types, default values, or nested structures. This often leads to scattered, repetitive, and brittle validation logic throughout an application.
Tanstack Router is built on the philosophy that search params are state and should be treated with the same care as any other state management system.
To understand the core problem that Tanstack Router solves, let's start with a blog post from the library's author.
Search Params Are State | TanStack Blog
This post, "Search Params Are State", frames the problem perfectly. It explains why traditional methods lead to fragmentation and how centralizing validation within the router provides a holistic solution.
Please read the first two sections, up to (but not including) "How TanStack Router Prevents Schema Fragmentation". Focus on the critique of traditional search param handling and the introduction of defining the schema inside the route.
This idea of centralizing the schema is analogous to defining an OpenAPI or GraphQL schema for an API. It creates a contract that the rest of the application can rely on, providing type safety and a single source of truth.
2. Validating Search Params with validateSearch and Zod
Tanstack Router's solution is the validateSearch option in a route definition. This function or schema runs every time the route is matched, giving you a single, reliable place to parse, validate, and provide defaults for all incoming search parameters.
While you can use any validation library, Zod is a popular and powerful choice due to its excellent type inference.
Let's dive into the official documentation to see how this is implemented.
Search Params | TanStack Router React Docs
The official documentation is the definitive guide. We'll focus on the sections that show how to implement validation with Zod.
Please read the sections on typing search params and the validation function. Pay close attention to the example that uses Zod and the distinction between .catch() and .default().
Implementing the Schema
As the documentation shows, you can define a Zod schema and pass it directly to validateSearch. Here’s a practical example for a product list page that supports pagination and sorting:
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router';
import { z } from 'zod';
// 1. Define the validation schema using Zod
const productSearchSchema = z.object({
page: z.number().int().positive().catch(1), // Fallback to page 1 if invalid
sortBy: z.enum(['price', 'name', 'createdAt']).catch('createdAt'), // Fallback to 'createdAt'
query: z.string().optional(), // A search query is optional
});
export const Route = createFileRoute('/products')({
// 2. Pass the schema to the route definition
validateSearch: productSearchSchema,
// ... loader and component will go here
});
Key Points:
.catch(defaultValue): This is the recommended approach for search parameters. If a value is missing or fails validation (e.g.,?page=abc), Zod will provide the fallback value (1in this case) instead of throwing an error. This creates a resilient user experience.- Error Handling Connection: If you were to use
.default()or omit a fallback, Zod would throw a validation error. Tanstack Router would catch this error and render the nearesterrorComponent—exactly the mechanism we learned about in the previous lesson. Using.catch()is a deliberate choice to handle invalid input gracefully without showing a full error page.
This video provides a great visual walkthrough of defining a schema and seeing how it provides type safety.
This clip from the "Why I Love TanStack Router" video demonstrates defining a Zod schema for search params and how it provides type safety in links.
Starting around five and a half minutes in, watch the search schema. Notice how a schema with defaults is defined and then used within the component via the useSearch hook. You'll also see how the router automatically falls back to the default value when the URL contains an invalid parameter.
3. The zodValidator Adapter
There's one more important piece to this puzzle. When you use Zod methods like .default() or .catch(), you create a schema where the input type can be different from the output type. For example, the page field in our schema above expects an optional number as input but guarantees a number as output.
This can cause TypeScript issues when you create links, as the router will expect you to provide properties that have defaults. To solve this, Tanstack provides an adapter.
Let's go back to the documentation for the solution.
Search Params | TanStack Router React Docs
This section of the documentation explains the need for the zodValidator adapter and shows how to use it. This is a crucial detail for a seamless developer experience.
Please read the subsection titled "Zod" under "Adapters" to review the Zod adapter. It explains why the adapter is necessary and how to use zodValidator to wrap your schema.
Here is our previous example, now updated to use the zodValidator:
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter'; // Import the adapter
import { z } from 'zod';
const productSearchSchema = z.object({
page: z.number().int().positive().catch(1),
sortBy: z.enum(['price', 'name', 'createdAt']).catch('createdAt'),
query: z.string().optional(),
});
export const Route = createFileRoute('/products')({
// Wrap the schema in the validator
validateSearch: zodValidator(productSearchSchema),
// ...
});
With this change, TypeScript will correctly understand that fields with defaults are optional when you use the <Link> component, while still guaranteeing that the validated search object will have those values defined.
4. Accessing Validated Params in Loaders
Now for the final step: using this validated data on the server.
Once validateSearch has run successfully, the resulting type-safe object is made available to the loader function in its context argument. You can simply destructure search and use it.
Here is a complete example showing how it all fits together:
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter';
import { z } from 'zod';
import { fetchProducts } from '../api/products'; // Assume this API client exists
// 1. Define the schema
const productSearchSchema = z.object({
page: z.number().int().positive().catch(1),
sortBy: z.enum(['price', 'name', 'createdAt']).catch('createdAt'),
query: z.string().optional(),
});
export const Route = createFileRoute('/products')({
// 2. Validate the search params
validateSearch: zodValidator(productSearchSchema),
// 3. Access the validated `search` object in the loader
loader: async ({ search }) => {
// `search` is fully typed: { page: number; sortBy: 'price' | 'name' | 'createdAt'; query?: string }
console.log('Fetching products with params:', search);
const products = await fetchProducts({
page: search.page,
sort: search.sortBy,
q: search.query,
});
return { products, searchParams: search };
},
// 4. The component can then use the loader data
component: ProductsComponent,
});
function ProductsComponent() {
const { products, searchParams } = Route.useLoaderData();
// ... render products and pagination/filter controls using searchParams
return (
<div>
<h1>Products</h1>
<p>Page: {searchParams.page}, Sort By: {searchParams.sortBy}</p>
{/* ... */}
</div>
);
}
As you can see, by the time your loader logic executes, the search object is guaranteed to be in the correct shape and type. There's no need for any additional parsing or validation within the loader itself, making your data-fetching code clean and robust.
Conclusion
In this lesson, we've established a powerful, type-safe pattern for managing URL state. By treating search parameters as a formal part of your route's contract, you eliminate a whole class of potential bugs and improve the developer experience significantly.
Key Takeaways:
- Tanstack Router treats URL search parameters as a first-class form of application state, not just strings.
- The
validateSearchroute option is the single source of truth for parsing, validating, and defining defaults for a route's search parameters. - Using Zod with
validateSearchprovides powerful validation and excellent type inference. - Graceful fallbacks can be implemented with Zod's
.catch()method, preventing malformed URLs from breaking the page. - The
zodValidatoradapter is essential for ensuring correct TypeScript types when using Zod features like.default()or.catch(). - The validated, type-safe
searchobject is passed directly to theloaderfunction, simplifying server-side data fetching logic.
We have now covered the essential pillars of Tanstack Router: file-based routing, server-side data loading with loaders, robust UI states with pendingComponent and errorComponent, and type-safe state management with validateSearch.
You are now well-equipped to move beyond the router itself and into the full-stack framework built around it. In our next lesson, we will begin Module 2 by initializing our first Tanstack Start project and exploring its structure for both Node.js and Bun runtimes.