Content Types
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).
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string () })).query ((opts ) => {return {greeting : `Hello ${opts .input .name }` };}),});
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string () })).query ((opts ) => {return {greeting : `Hello ${opts .input .name }` };}),});
Non-JSON Content Types
In addition to JSON, tRPC can use FormData, File, and other binary types as procedure inputs.
Client Setup
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 of the box — if you're only using this link, your existing setup should work immediately.
tsimport {createTRPCClient ,httpLink } from '@trpc/client';import type {AppRouter } from './server';createTRPCClient <AppRouter >({links : [httpLink ({url : 'http://localhost:2022',}),],});
tsimport {createTRPCClient ,httpLink } from '@trpc/client';import type {AppRouter } from './server';createTRPCClient <AppRouter >({links : [httpLink ({url : '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.
tsimport {createTRPCClient ,httpBatchLink ,httpLink ,isNonJsonSerializable ,splitLink ,} from '@trpc/client';import type {AppRouter } from './server';consturl = 'http://localhost:2022';createTRPCClient <AppRouter >({links : [splitLink ({condition : (op ) =>isNonJsonSerializable (op .input ),true :httpLink ({url ,}),false :httpBatchLink ({url ,}),}),],});
tsimport {createTRPCClient ,httpBatchLink ,httpLink ,isNonJsonSerializable ,splitLink ,} from '@trpc/client';import type {AppRouter } from './server';consturl = 'http://localhost:2022';createTRPCClient <AppRouter >({links : [splitLink ({condition : (op ) =>isNonJsonSerializable (op .input ),true :httpLink ({url ,}),false :httpBatchLink ({url ,}),}),],});
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:
tsimport {createTRPCClient ,httpBatchLink ,httpLink ,isNonJsonSerializable ,splitLink ,} from '@trpc/client';importsuperjson from 'superjson';import type {AppRouter } from './server';consturl = 'http://localhost:2022';createTRPCClient <AppRouter >({links : [splitLink ({condition : (op ) =>isNonJsonSerializable (op .input ),true :httpLink ({url ,transformer : {// request - convert data before sending to the tRPC serverserialize : (data ) =>data ,// response - convert the tRPC response before using it in clientdeserialize : (data ) =>superjson .deserialize (data ), // or your other transformer},}),false :httpBatchLink ({url ,transformer :superjson , // or your other transformer}),}),],});
tsimport {createTRPCClient ,httpBatchLink ,httpLink ,isNonJsonSerializable ,splitLink ,} from '@trpc/client';importsuperjson from 'superjson';import type {AppRouter } from './server';consturl = 'http://localhost:2022';createTRPCClient <AppRouter >({links : [splitLink ({condition : (op ) =>isNonJsonSerializable (op .input ),true :httpLink ({url ,transformer : {// request - convert data before sending to the tRPC serverserialize : (data ) =>data ,// response - convert the tRPC response before using it in clientdeserialize : (data ) =>superjson .deserialize (data ), // or your other transformer},}),false :httpBatchLink ({url ,transformer :superjson , // or your other transformer}),}),],});
Server Setup
When a request is handled by tRPC, it takes care of parsing the request body based on the Content-Type header of the request.
If you encounter errors like Failed to parse body as XXX, make sure that your server (e.g., Express, Next.js) isn't parsing the request body before tRPC handles it.
ts// Example in expressimportexpress from 'express';import * astrpcExpress from '@trpc/server/adapters/express';import {appRouter } from './router';// incorrectconstapp1 =express ();app1 .use (express .json ()); // this tries to parse body before tRPC.app1 .post ('/express/hello', (req ,res ) => {res .end (); }); // normal express route handlerapp1 .use ('/trpc',trpcExpress .createExpressMiddleware ({router :appRouter })); // tRPC fails to parse body// correctconstapp2 =express ();app2 .use ('/express',express .json ()); // do it only in "/express/*" pathapp2 .post ('/express/hello', (req ,res ) => {res .end (); });app2 .use ('/trpc',trpcExpress .createExpressMiddleware ({router :appRouter })); // tRPC can parse body
ts// Example in expressimportexpress from 'express';import * astrpcExpress from '@trpc/server/adapters/express';import {appRouter } from './router';// incorrectconstapp1 =express ();app1 .use (express .json ()); // this tries to parse body before tRPC.app1 .post ('/express/hello', (req ,res ) => {res .end (); }); // normal express route handlerapp1 .use ('/trpc',trpcExpress .createExpressMiddleware ({router :appRouter })); // tRPC fails to parse body// correctconstapp2 =express ();app2 .use ('/express',express .json ()); // do it only in "/express/*" pathapp2 .post ('/express/hello', (req ,res ) => {res .end (); });app2 .use ('/trpc',trpcExpress .createExpressMiddleware ({router :appRouter })); // tRPC can parse body
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.
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .instanceof (FormData )).mutation ((opts ) => {constdata =opts .input ;return {greeting : `Hello ${data .get ('name')}`,};}),});
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .instanceof (FormData )).mutation ((opts ) => {constdata =opts .input ;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.
tsimport {octetInputParser } from '@trpc/server/http';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({upload :publicProcedure .input (octetInputParser ).mutation ((opts ) => {constdata =opts .input ;return {valid : true,};}),});
tsimport {octetInputParser } from '@trpc/server/http';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({upload :publicProcedure .input (octetInputParser ).mutation ((opts ) => {constdata =opts .input ;return {valid : true,};}),});