Input validation with input parsers
Define input parsers on publicProcedure.input() to validate and parse procedure inputs. The input parser should validate and cast the input, returning a strongly typed value for valid input or throwing an error for invalid input. Input parsers can be custom functions, Zod schemas, Yup schemas, or Valibot schemas.
Custom input parser example
publicProcedure
.input((val: unknown) => {
if (typeof val === 'string') return val;
throw new Error(`Invalid input: ${typeof val}`);
})
.query(async (opts) => {
const { input } = opts;
const user: User = { id: input, name: 'Katt' };
return user;
});
Zod input validation example
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
publicProcedure
.input(z.string())
.query(async (opts) => {
const { input } = opts;
const user: User = { id: input, name: 'Katt' };
return user;
});
Data transformers serialize responses and input arguments
Data transformers allow you to serialize the response data and input arguments. The transformers need to be added both to the server and the client.
SuperJSON enables transparent serialization of Date, Map, Set types
SuperJSON allows transparent use of standard Date, Map, and Set objects over the wire between the server and client. You can return any of these types from your API resolver and use them in the client without having to recreate the objects from JSON.
SuperJSON setup in initTRPC
To use SuperJSON, install it with 'yarn add superjson', then add it to initTRPC by setting transformer: superjson in the create() call. TypeScript will guide you to add the transformer to httpLink() and other links.
SuperJSON client setup example
import { createTRPCClient, httpLink } from '@trpc/client';
import type { AppRouter } from './server';
import superjson from 'superjson';
export const client = createTRPCClient<AppRouter>({
links: [
httpLink({
url: 'http://localhost:3000',
transformer: superjson,
}),
],
});
Devalue as alternative to SuperJSON
Devalue works like SuperJSON, focusing on performance and compact payloads, but at the cost of a less human-readable body.
Devalue setup with XSS mitigation
To use devalue with XSS mitigation, create a transformer object with deserialize and serialize functions. The deserialize function should use devalue's parse() method and serialize should use stringify(). This setup should be added to both initTRPC on the server and httpLink() on the client.
Devalue transformer implementation example
import { parse, stringify } from 'devalue';
export const transformer = {
deserialize: (object: any) => parse(object),
serialize: (object: any) => stringify(object),
};
Different transformers for upload and download
You can provide individual transformers for upload and download by using different transformers for one direction or different transformers for each direction (for example, for performance reasons). Make sure you use the same combined transformer everywhere.
DataTransformer interface
export interface DataTransformer {
serialize(object: any): any;
deserialize(object: any): any;
}
OutputDataTransformer interface
interface OutputDataTransformer extends DataTransformer {
serialize(object: any): any; // runs on the server before sending data to the client
deserialize(object: any): any; // runs only on the client to transform data from server
}
CombinedDataTransformer interface
export interface CombinedDataTransformer {
input: InputDataTransformer; // specifies how data from client to server is transformed
output: OutputDataTransformer; // specifies how data from server to client is transformed
}
tRPC supported content types for procedure inputs
tRPC supports multiple content types as procedure inputs: JSON-serializable data, FormData, File, Blob, and other binary types.
JSON is tRPC default content type
By default, tRPC sends and receives JSON-serializable data. No extra configuration is needed, and any input that can be serialized to JSON works out of the box with all links (httpLink, httpBatchLink, httpBatchStreamLink).
JSON procedure input example
Example of a tRPC procedure with JSON input validation using Zod:
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure.input(z.object({ name: z.string() })).query((opts) => {
return { greeting: `Hello ${opts.input.name}` };
}),
});
```
FormData is natively supported in tRPC
FormData is natively supported as a procedure input in tRPC. You can validate it with z.instanceof(FormData) in Zod, and optionally use a library like zod-form-data for more advanced type-safe validation.
FormData procedure input example
Example of a tRPC procedure with FormData input:
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure.input(z.instanceof(FormData)).mutation((opts) => {
const data = opts.input;
return {
greeting: `Hello ${data.get('name')}`,
};
}),
});
```
Binary type inputs with octetInputParser
tRPC converts many octet content types to a ReadableStream which can be consumed in a procedure. Currently supported binary types are Blob, Uint8Array, and File. Use the octetInputParser from @trpc/server/http to handle these types.
Binary type file upload procedure example
Example of a tRPC procedure that handles file uploads as binary data:
```ts
import { initTRPC } from '@trpc/server';
import { octetInputParser } from '@trpc/server/http';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
upload: publicProcedure.input(octetInputParser).mutation((opts) => {
const data = opts.input;
return {
valid: true,
};
}),
});
```
Output validation for subscriptions
Since subscriptions are async iterators, you have to go through the iterator to validate the output. A custom Zod schema helper function called zAsyncIterable can be created to validate both the yielded values and the return value of an async iterable used by subscription procedures.
zAsyncIterable Zod helper for subscription validation
zAsyncIterable is a Zod schema helper for validating async iterables. It accepts an options object with: yield (required) - a ZodType for validating values yielded by the async generator; return (optional) - a ZodType for validating the return value; tracked (optional, boolean) - whether the yielded values are tracked (for subscriptions). When tracked is true, the helper validates that each yielded value is a TrackedEnvelope containing an id and data.
zAsyncIterable implementation example
Complete implementation of zAsyncIterable helper:
```ts
import type { TrackedEnvelope } from '@trpc/server';
import { isTrackedEnvelope, tracked } from '@trpc/server';
import { z } from 'zod';
function isAsyncIterable<TValue, TReturn = unknown>(
value: unknown,
): value is AsyncIterable<TValue, TReturn> {
return !!value && typeof value === 'object' && Symbol.asyncIterator in value;
}
const trackedEnvelopeSchema =
z.custom<TrackedEnvelope<unknown>>(isTrackedEnvelope);
export function zAsyncIterable<
TYieldIn,
TYieldOut,
TReturnIn = void,
TReturnOut = void,
Tracked extends boolean = false,
>(opts: {
yield: z.ZodType<TYieldOut, TYieldIn>;
return?: z.ZodType<TReturnOut, TReturnIn>;
tracked?: Tracked;
}) {
return z
.custom<
AsyncIterable<
Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn,
TReturnIn
>
>((val) => isAsyncIterable(val))
.transform(async function* (iter) {
const iterator = iter[Symbol.asyncIterator]();
try {
let next;
while ((next = await iterator.next()) && !next.done) {
if (opts.tracked) {
const [id, data] = trackedEnvelopeSchema.parse(next.value);
yield tracked(id, await opts.yield.parseAsync(data));
continue;
}
yield opts.yield.parseAsync(next.value);
}
if (opts.return) {
return await opts.return.parseAsync(next.value);
}
return;
} finally {
await iterator.return?.();
}
}) as z.ZodType<
AsyncIterable<
Tracked extends true ? TrackedEnvelope<TYieldIn> : TYieldIn,
TReturnIn,
unknown
>,
AsyncIterable<
Tracked extends true ? TrackedEnvelope<TYieldOut> : TYieldOut,
TReturnOut,
unknown
>
>;
}
```
Subscription procedure with zAsyncIterable output validation
Example of using zAsyncIterable to validate subscription output:
```ts
import { tracked } from '@trpc/server';
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
import { zAsyncIterable } from './zAsyncIterable';
export const appRouter = router({
mySubscription: publicProcedure
.input(
z.object({
lastEventId: z.coerce.number().min(0).optional(),
}),
)
.output(
zAsyncIterable({
yield: z.object({
count: z.number(),
}),
tracked: true,
}),
)
.subscription(async function* (opts) {
let index = opts.input.lastEventId ?? 0;
while (true) {
index++;
yield tracked(String(index), {
count: index,
});
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}),
});
```
The output validator ensures that each yielded value is a tracked envelope containing an id and a data object with a count property.
Optional input fields with Zod
Mark optional input fields using z.string().optional() in the Zod schema. When omitted by the client, the input value will be undefined.
Input validators with procedure.input() method
Input validators are defined using the `procedure.input()` method. This method takes a validator (such as a Zod schema) and enables tRPC to check that a procedure call is correct and return a validation error if validation fails. The input validator also enables type inference for the input parameter accessed via `opts.input`.
Input merging by stacking .input() calls
Multiple `.input()` calls can be stacked to build more complex input types. Input merging works by spreading object properties together, which means only object types can be chained—non-object types (like `z.string()`) cannot be merged. If two chained `.input()` calls define the same property, the later one takes precedence. This pattern is particularly useful when you want to utilise common input to a collection of procedures in a middleware.
Output validators with procedure.output() method
Output validators are defined using the `procedure.output()` method. Output validation checks that data returned from untrusted sources is correct and ensures that you are not returning more data to the client than necessary. If output validation fails, the server will respond with an `INTERNAL_SERVER_ERROR`.
Function validators without third-party libraries
A validator can be defined as a simple function that takes a value and returns the validated value or throws an error. The function receives the input/output value and must return the typed value or throw an error if validation fails. While this approach works without third-party dependencies, using a validation library is recommended in most cases.
Standard Schema interface for validators
tRPC uses the Standard Schema interface if available for type inference, or custom interfaces for supported validators. Conforming to Standard Schema is the recommended approach for new validator library integrations.
Zod validator integration example
Example of using Zod with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.output(
z.object({
greeting: z.string(),
}),
)
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
Zod is the default recommendation for validation in tRPC projects.
Yup validator integration example
Example of using Yup with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import * as yup from 'yup';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
yup.object({
name: yup.string().required(),
}),
)
.output(
yup.object({
greeting: yup.string().required(),
}),
)
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
Superstruct validator integration example
Example of using Superstruct with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import { object, string } from 'superstruct';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(object({ name: string() }))
.output(object({ greeting: string() }))
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
Valibot validator integration example
Example of using Valibot with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import * as v from 'valibot';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(v.object({ name: v.string() }))
.output(v.object({ greeting: v.string() }))
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
ArkType validator integration example
Example of using ArkType with tRPC input validation:
```ts
import { initTRPC } from '@trpc/server';
import { type } from 'arktype';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure.input(type({ name: 'string' })).query((opts) => {
return {
greeting: `hello ${opts.input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
Effect Schema validator integration example
Example of using Effect Schema with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import { Schema } from 'effect';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(Schema.standardSchemaV1(Schema.Struct({ name: Schema.String })))
.output(Schema.standardSchemaV1(Schema.Struct({ greeting: Schema.String })))
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
Typia validator integration example
Example of using Typia with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import typia from 'typia';
import { v4 } from 'uuid';
import { IBbsArticle } from '../structures/IBbsArticle';
const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
store: publicProcedure
.input(typia.createAssert<IBbsArticle.IStore>())
.output(typia.createAssert<IBbsArticle>())
.query(({ input }) => {
return {
id: v4(),
writer: input.writer,
title: input.title,
body: input.body,
created_at: new Date().toString(),
};
}),
});
export type AppRouter = typeof appRouter;
```
scale-ts validator integration example
Example of using scale-ts with tRPC input and output validation:
```ts
import { initTRPC } from '@trpc/server';
import * as $ from 'scale-codec';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input($.object($.field('name', $.str)))
.output($.object($.field('greeting', $.str)))
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
TypeBox validator integration example
Example of using TypeBox with tRPC input and output validation:
```ts
import { Type } from '@sinclair/typebox';
import { initTRPC } from '@trpc/server';
import { wrap } from '@typeschema/typebox';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(wrap(Type.Object({ name: Type.String() })))
.output(wrap(Type.Object({ greeting: Type.String() })))
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
@robolex/sure validator integration example
Example of using @robolex/sure with tRPC input and output validation:
```ts
import { err, object, string } from '@robolex/sure';
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input(
err(
object({
name: string,
}),
),
)
.output(
err(
object({
greeting: string,
}),
),
)
.query(({ input }) => {
return {
greeting: `hello ${input.name}`,
};
}),
});
export type AppRouter = typeof appRouter;
```
The @robolex/sure library provides an `err` function that wraps a schema and throws on validation failure. You can define custom error types and error throwing functions as needed.
Function validator with input and output
Example of defining input and output validators as functions:
```ts
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const publicProcedure = t.procedure;
export const appRouter = t.router({
hello: publicProcedure
.input((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Input is not a string');
})
.output((value): string => {
if (typeof value === 'string') {
return value;
}
throw new Error('Output is not a string');
})
.query((opts) => {
const { input } = opts;
return `hello ${input}`;
}),
});
export type AppRouter = typeof appRouter;
```
Input merging in middleware example
Example of input merging using stacked `.input()` calls in a middleware pattern:
```ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
const baseProcedure = t.procedure
.input(z.object({ townName: z.string() }))
.use((opts) => {
const input = opts.input;
console.log(`Handling request with user from: ${input.townName}`);
return opts.next();
});
export const appRouter = t.router({
hello: baseProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const input = opts.input;
return {
greeting: `Hello ${input.name}, my friend from ${input.townName}`,
};
}),
});
```
The input from the base procedure (`townName`) is merged with the input from the specific procedure (`name`), resulting in a combined input object with both properties.
tRPC input and output validation
Add input validation to procedures using .input(validator) and output validation using .output(validator). Use Zod or other libraries for validators skill.
tRPC FormData and binary content uploads
Accept FormData, File, Blob, or binary uploads in mutations. Refer to non-json-content-types skill for implementation details.
Output validation is optional but prevents accidental data leaks
tRPC provides automatic type-safety of outputs without requiring a validator. However, output validation can be useful to strictly define the output type and prevent sensitive data from being leaked. Output validation is entirely optional.
Output validation on query and mutation router methods
Similar to input validation, an output: validation can be added to the query() and mutation() router methods. The output validator is invoked with the payload returned by the resolve() function.
Output validator inferred type must match resolve return type
When an output validator is defined, its inferred type is expected as the return type of the resolve() function.
Supported output validators in tRPC
tRPC works out-of-the-box with yup, superstruct, zod, myzod, and custom validators.
Output validation example with Zod
import * as trpc from '@trpc/server';
import { z } from 'zod';
export const appRouter = trpc.router<Context>().query('hello', {
output: z.object({
greeting: z.string(),
}),
// expects return type of { greeting: string }
resolve() {
return {
greeting: 'hello!',
};
},
});
export type AppRouter = typeof appRouter;
Output validation example with Yup
import * as trpc from '@trpc/server';
import * as yup from 'yup';
export const appRouter = trpc.router<Context>().query('hello', {
output: yup.object({
greeting: yup.string().required(),
}),
resolve() {
return { greeting: 'hello!' };
},
});
export type AppRouter = typeof appRouter;
Output validation example with Superstruct
import * as trpc from '@trpc/server';
import * as t from 'superstruct';
export const appRouter = trpc.router<Context>().query('hello', {
input: t.string(),
output: t.object({
greeting: t.string(),
}),
resolve({ input }) {
return { greeting: input };
},
});
export type AppRouter = typeof appRouter;
Output validation example with custom validator
import * as trpc from '@trpc/server';
import * as t from 'superstruct';
export const appRouter = trpc.router<Context>().query('hello', {
output: (value: any) => {
if (value && typeof value.greeting === 'string') {
return { greeting: value.greeting };
}
throw new Error('Greeting not found');
},
// expects return type of { greeting: string }
resolve() {
return { greeting: 'hello!' };
},
});
export type AppRouter = typeof appRouter;
tRPC supports FormData
tRPC can be used with FormData, as demonstrated in an example project that shows how to integrate FormData handling with tRPC in a Next.js application.