Skip to content
tRPC

Partners

Content Types

On this page

tRPC supports multiple content types as procedure inputs: JSON-serializable data, FormData, File, Blob, and other binary types.

JSON (Default)

By default, tRPC sends and receives JSON-serializable data. No extra configuration is needed — any input that can be serialized to JSON works out of the box with all links (httpLink, httpBatchLink, httpBatchStreamLink).


import { import zz } from 'zod';

export const 
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
const const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            name: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
    hello: QueryProcedure<{
        input: {
            name: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    hello: QueryProcedure<{
        input: {
            name: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: QueryProcedure<{
        input: {
            name: string;
        };
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
hello: QueryProcedure<{
    input: {
        name: string;
    };
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
hello
: const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure.
ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    name: string;
}, {
    name: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(import zz.
function object<{
    name: z.ZodString;
}>(shape?: {
    name: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>
object
({ name: z.ZodStringname: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() })).
ProcedureBuilder<object, object, object, { name: string; }, { name: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
    greeting: string;
}>(resolver: ProcedureResolver<object, object, object, {
    name: string;
}, UnsetMarker, {
    greeting: string;
}>): QueryProcedure<{
    input: {
        name: string;
    };
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
query
((
opts: ProcedureResolverOptions<object, object, object, {
    name: string;
}>
opts
) => {
return { greeting: stringgreeting: `Hello ${
opts: ProcedureResolverOptions<object, object, object, {
    name: string;
}>
opts
.
ProcedureResolverOptions<object, object, object, { name: string; }>.input: {
    name: string;
}
input
.name: stringname}` };
}), });

Non-JSON Content Types

In addition to JSON, tRPC can use FormData, File, and other binary types as procedure inputs.

Client Setup

httpLink supports non-JSON content types out of the box — if you're only using this link, your existing setup should work immediately.

import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpLink
({
url: string | URLurl: 'http://localhost:2022', }), ], });

However, not all links support these content types. If you're using httpBatchLink or httpBatchStreamLink, you will need to include a splitLink and route requests based on the content type.

import {
  function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
  function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
  function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink,
  function isNonJsonSerializable(input: unknown): input is Blob | FormData | Uint8Array<ArrayBuffer>isNonJsonSerializable,
  
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
    false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink
,
} from '@trpc/client'; import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
const const url: "http://localhost:2022"url = 'http://localhost:2022';
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
splitLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<...>>>[];
    false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink
({
condition: (op: Operation) => booleancondition: (op: Operationop) => function isNonJsonSerializable(input: unknown): input is Blob | FormData | Uint8Array<ArrayBuffer>isNonJsonSerializable(op: Operationop.input: unknowninput),
true: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
true
:
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpLink
({
url: string | URLurl, }),
false: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
false
:
httpBatchLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink
({
url: string | URLurl, }), }), ], });

If you are using transformer in your tRPC server, TypeScript requires that your tRPC client link defines transformer as well. Use this example as a base:

import {
  function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
  function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
  function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink,
  function isNonJsonSerializable(input: unknown): input is Blob | FormData | Uint8Array<ArrayBuffer>isNonJsonSerializable,
  
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
    false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink
,
} from '@trpc/client'; import class superjsonsuperjson from 'superjson'; import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
const const url: "http://localhost:2022"url = 'http://localhost:2022';
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
splitLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<...>>>[];
    false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink
({
condition: (op: Operation) => booleancondition: (op: Operationop) => function isNonJsonSerializable(input: unknown): input is Blob | FormData | Uint8Array<ArrayBuffer>isNonJsonSerializable(op: Operationop.input: unknowninput),
true: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
true
:
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
httpLink
({
url: string | URLurl, transformer: DataTransformerOptionstransformer: { // request - convert data before sending to the tRPC server DataTransformer.serialize(object: any): anyserialize: (data: anydata) => data: anydata, // response - convert the tRPC response before using it in client DataTransformer.deserialize(object: any): anydeserialize: (data: anydata) => class superjsonsuperjson.
SuperJSON.deserialize: <any>(payload: SuperJSONResult, options?: {
    inPlace?: boolean;
}) => any
deserialize
(data: anydata), // or your other transformer
}, }),
false: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
false
:
httpBatchLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink
({
url: string | URLurl, transformer: DataTransformerOptionstransformer: class superjsonsuperjson, // or your other transformer }), }), ], });

Server Setup

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.


import { import zz } from 'zod';

export const 
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
const const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: MutationProcedure<{
        input: FormData;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
    hello: MutationProcedure<{
        input: FormData;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    hello: MutationProcedure<{
        input: FormData;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: MutationProcedure<{
        input: FormData;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
hello: MutationProcedure<{
    input: FormData;
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
hello
: const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodInstanceOf<FormData>>(schema: z.ZodInstanceOf<FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false, DefaultErrorShape>input(import zz.
instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodInstanceOf<FormData>
export instanceof
instanceof
(
var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}
FormData
)).
ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.mutation<{
    greeting: string;
}>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, {
    greeting: string;
}>): MutationProcedure<{
    input: FormData;
    output: {
        greeting: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
mutation
((opts: ProcedureResolverOptions<object, object, object, FormData>opts) => {
const data = opts: ProcedureResolverOptions<object, object, object, FormData>opts.ProcedureResolverOptions<object, object, object, FormData>.input: FormDatainput;
const data: FormData
return { greeting: stringgreeting: `Hello ${const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('name')}`, }; }), });

On the client, pass a FormData instance to the mutation:

const const form: FormDataform = new var FormData: new (form?: HTMLFormElement, submitter?: HTMLElement | null) => FormDataFormData();
const form: FormDataform.FormData.append(name: string, value: string | Blob): void (+2 overloads)append('name', 'Bilbo');

const result = await 
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    hello: MutationProcedure<{
        input: FormData;
        output: {
            greeting: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>
client
.
hello: {
    mutate: Resolver<{
        input: FormData;
        output: {
            greeting: string;
        };
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}
hello
.
mutate: (input: FormData, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
    greeting: string;
}, DefaultErrorShape>
mutate
(const form: FormDataform);
const result: {
    greeting: string;
}

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.


import { const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>octetInputParser } from '@trpc/server/http';

export const 
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
const const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    upload: MutationProcedure<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
    upload: MutationProcedure<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    upload: MutationProcedure<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    upload: MutationProcedure<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
router
({
upload: MutationProcedure<{
    input: OctetInput;
    output: {
        bytes: number;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
upload
: const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false, DefaultErrorShape>input(const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>octetInputParser).
ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.mutation<{
    bytes: number;
}>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, {
    bytes: number;
}>): MutationProcedure<{
    input: OctetInput;
    output: {
        bytes: number;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
mutation
(async (opts: ProcedureResolverOptions<object, object, object, ReadableStream<any>>opts) => {
const data = opts: ProcedureResolverOptions<object, object, object, ReadableStream<any>>opts.ProcedureResolverOptions<object, object, object, ReadableStream<any>>.input: ReadableStream<any>input;
const data: ReadableStream<any>
const const reader: ReadableStreamDefaultReader<any>reader = const data: ReadableStream<any>data.ReadableStream<any>.getReader(): ReadableStreamDefaultReader<any> (+2 overloads)getReader(); let let bytes: numberbytes = 0; while (true) { const { const done: booleandone, const value: anyvalue } = await const reader: ReadableStreamDefaultReader<any>reader.ReadableStreamDefaultReader<any>.read(): Promise<ReadableStreamReadResult<any>>read(); if (const done: booleandone) break; let bytes: numberbytes += const value: anyvalue.byteLength; } return { bytes: numberbytes }; }), });

On the client, pass a File, Blob or Uint8Array to the mutation:

const const file: Filefile = new var File: new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) => FileFile(['hello world'], 'hello.txt', { BlobPropertyBag.type?: string | undefinedtype: 'text/plain' });

await 
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    upload: MutationProcedure<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>
client
.
upload: {
    mutate: Resolver<{
        input: OctetInput;
        output: {
            bytes: number;
        };
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}
upload
.
mutate: (input: OctetInput, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
    bytes: number;
}, DefaultErrorShape>
mutate
(const file: Filefile);

Common mistakes

  • Using a query. FormData and binary inputs are sent as the body of a POST request, so they only work with mutations: for a query, httpLink throws FormData is only supported for mutations or Octet type input is only supported for mutations. The exception is an httpLink with methodOverride: 'POST', which sends every call as a POST and so also sends FormData and binary inputs for queries; the server must allow the method override.

  • Sending them through a batching link. httpBatchLink, httpBatchStreamLink and wsLink (with its default encoder) serialize inputs as JSON, which turns a FormData or File into {}, so input validation fails. Route them to httpLink with a splitLink. Only httpLink sends non-JSON inputs:

    Linkapplication/jsonFormDataFile / Blob / Uint8Array
    httpLink✅✅ (POST)✅ (POST)
    httpBatchLink✅❌❌
    httpBatchStreamLink✅❌❌
    wsLink✅❌❌

    With a binary experimental_encoder such as MessagePack, wsLink can carry Uint8Array values, as in the WebSocket encoder example.

  • Transforming the request. tRPC passes FormData and binary inputs to your procedure as they are, without your transformer. If you use one, give the non-JSON httpLink a transformer that leaves the request alone, as shown above.

  • Letting your server parse the body first. See Server Setup.