Skip to main content
Version: 11.x

Non-JSON Content Types

In addition to JSON-serializable data, tRPC can use FormData, File, and other Binary types as procedure inputs

Client Setup

info

While tRPC natively supports several non-json serializable types, your client may need a little link configuration to support them depending on your setup.

httpLink supports non-json content types out the box, if you're only using this then your existing setup should work immediately

ts
import { httpLink } from '@trpc/client';
trpc.createClient({
links: [
httpLink({
url: 'http://localhost:2022',
}),
],
});
ts
import { httpLink } from '@trpc/client';
trpc.createClient({
links: [
httpLink({
url: 'http://localhost:2022',
}),
],
});

However, not all links support these new content types, if you're using httpBatchLink or httpBatchStreamLink you will need to include a splitLink and check which link to use depending on the content

ts
import {
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
trpc.createClient({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({
url,
}),
false: httpBatchLink({
url,
}),
}),
],
});
ts
import {
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
trpc.createClient({
links: [
splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLink({
url,
}),
false: httpBatchLink({
url,
}),
}),
],
});

FormData Input

FormData is natively supported, and for more advanced usage you could also combine this with a library like zod-form-data to validate inputs in a type-safe way.

ts
import { z } from 'zod';
 
export const t = initTRPC.create();
const publicProcedure = t.procedure;
 
export const appRouter = t.router({
hello: publicProcedure.input(z.instanceof(FormData)).query((opts) => {
const data = opts.input;
const data: FormData
return {
greeting: `Hello ${data.get('name')}`,
};
}),
});
ts
import { z } from 'zod';
 
export const t = initTRPC.create();
const publicProcedure = t.procedure;
 
export const appRouter = t.router({
hello: publicProcedure.input(z.instanceof(FormData)).query((opts) => {
const data = opts.input;
const data: FormData
return {
greeting: `Hello ${data.get('name')}`,
};
}),
});

For a more advanced code sample you can see our example project here

File and other Binary Type Inputs

tRPC converts many octet content types to a ReadableStream which can be consumed in a procedure. Currently these are Blob Uint8Array and File.

ts
import { octetInputParser } from '@trpc/server/http';
 
export const t = initTRPC.create();
const publicProcedure = t.procedure;
 
export const appRouter = t.router({
upload: publicProcedure.input(octetInputParser).query((opts) => {
const data = opts.input;
const data: ReadableStream<any>
return {
valid: true,
};
}),
});
ts
import { octetInputParser } from '@trpc/server/http';
 
export const t = initTRPC.create();
const publicProcedure = t.procedure;
 
export const appRouter = t.router({
upload: publicProcedure.input(octetInputParser).query((opts) => {
const data = opts.input;
const data: ReadableStream<any>
return {
valid: true,
};
}),
});