Data Validation in the TanStack Start Prisma SaaS Kit
Learn how to validate data in the TanStack Start Prisma SaaS Kit.
Data Validation is a crucial aspect of building secure applications. In this section, we will look at how to validate data in the TanStack Start Prisma SaaS Kit.
Steps to validate data
Learn how to validate data in the TanStack Start Prisma SaaS Kit.
What are we validating?
A general rule, is that all client-side provided data should be validated/sanitized. This includes:
- URL parameters
- Search params
- Form data
- Cookies
- Any data provided by the user
Using Zod to validate data
The TanStack Start Prisma SaaS Kit uses Zod to validate data. You can use the z object to validate data in your application.
import { z } from "zod";const userSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(),});// Validate the dataconst validatedData = userSchema.parse(data);Validating payload data to Server Side API
We generally use server functions (createServerFn) for sending data from a client to a server. The TanStack Start Prisma SaaS Kit uses the @kit/function-middleware middleware tuples, which wire validation and authentication into the standard createServerFn chain.
Server functions: Using the server-function middleware tuples
createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware) is a pre-composed tuple that ensures the user is authenticated. Add .validator(schema) to validate input against a Zod schema and .handler(...) to implement the function.
import { createServerFn } from "@tanstack/react-start";import { authFunctionMiddleware } from "@kit/function-middleware/functions";import { z } from "zod";const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(),});export const createUser = createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware) .validator(UserSchema) .handler(async ({ data, context }) => { // `data` is now validated against the UserSchema // `context.user` is the authenticated user // do something with the validated data });When you define an function using createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware) with .validator(...), the data argument is validated against the schema automatically, without you having to do anything else. In addition, context.user and context.session are available to you, which contain the authenticated user and session data.
Call the function from a client component with TanStack Query:
import { useMutation } from '@tanstack/react-query';import { createUser } from './create-user.functions';const mutation = useMutation({ mutationFn: createUser });mutation.mutate({ data: { id, name, email } });Organization-scoped functions
For functions that require organization context, use createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware):
import { createServerFn } from "@tanstack/react-start";import { organizationFunctionMiddleware } from "@kit/function-middleware/functions";import { z } from "zod";const UpdateDocumentSchema = z.object({ documentId: z.string().uuid(), content: z.string(),});export const updateDocument = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware) .validator(UpdateDocumentSchema) .handler(async ({ data, context }) => { // `data` is validated // `context.user` is the authenticated user // `context.organizationId` is the current organization // `context.role` is the user's role in the organization // do something with the validated data });Functions without input
For functions that don't require input validation, omit .validator(...):
import { createServerFn } from "@tanstack/react-start";import { authFunctionMiddleware } from "@kit/function-middleware/functions";export const getCurrentUser = createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware) .handler(async ({ context }) => { // No input needed, just return user data return context.user; });Role-protected functions
For functions that require a minimum role level, layer the withMinRole middleware:
import { organizationFunctionMiddleware } from "@kit/function-middleware/functions";import { withMinRole } from "@kit/function-middleware/server";import { z } from "zod";const DeleteMemberSchema = z.object({ memberId: z.string().uuid(),});export const deleteMember = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware) .middleware([withMinRole('admin')]) .validator(DeleteMemberSchema) .handler(async ({ data, context }) => { // Only admins and owners can reach this point // `data` is validated });Permission-protected functions
For functions that require specific permissions, layer the withFeaturePermission middleware:
import { organizationFunctionMiddleware } from "@kit/function-middleware/functions";import { withFeaturePermission } from "@kit/function-middleware/server";import { z } from "zod";const UpdateBillingSchema = z.object({ planId: z.string(),});export const updateBilling = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware) .middleware([withFeaturePermission({ billing: ['update'] })]) .validator(UpdateBillingSchema) .handler(async ({ data, context }) => { // Only users with billing:update permission can reach this point });Validating Cookies
Whenever you use a cookie in your application, you should validate the cookie data.
On the server (inside a server function, loader, or middleware), read cookies with getCookie from @tanstack/react-start/server. Let's assume you receive a cookie with the name my-cookie, and you expect it to be a number. You can validate the cookie data as follows:
import { z } from "zod";import { getCookie } from "@tanstack/react-start/server";const cookie = z.coerce.number() .safeParse(getCookie("my-cookie"));if (!cookie.success) { // handle the error or provide a default value}Validating URL parameters
Whenever you receive a search parameter, you should validate it. TanStack Router validates search params at the route boundary with validateSearch, so the rest of your route only ever sees validated values. Let's assume you receive a search parameter named page, and you expect it to be a number:
import { createFileRoute } from "@tanstack/react-router";export const Route = createFileRoute('/items/')({ validateSearch: (search: Record<string, unknown>) => ({ page: search.page ? Number(search.page) : 0, }), loaderDeps: ({ search }) => ({ page: search.page }), loader: ({ deps }) => getItems({ data: deps.page }),});You can also validate with a Zod schema for stricter parsing and defaults:
import { createFileRoute } from "@tanstack/react-router";import { z } from "zod";const SearchSchema = z.object({ page: z.coerce.number().min(0).catch(0),});export const Route = createFileRoute('/items/')({ validateSearch: (search) => SearchSchema.parse(search),});In components, read the validated values with Route.useSearch(); for dynamic path segments use Route.useParams(). Because validation happens at the route boundary, your component receives already-validated, typed data.
Going forward, remember to validate all data that you receive from the client, and never trust anything the client provides you with. Always have a default value ready to handle invalid data (the .catch(...) above is a convenient way to do this), which can prevent potential security issues or bugs in your application.