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 data
const 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.