Non-JSON Content Types
In addition to JSON-serializable data, 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 the box, if you're only using this then your existing setup should work immediately
tsimport { httpLink } from '@trpc/client';trpc.createClient({links: [httpLink({url: 'http://localhost:2022',}),],});
tsimport { 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
tsimport {httpBatchLink,httpLink,isNonJsonSerializable,splitLink,} from '@trpc/client';trpc.createClient({links: [splitLink({condition: (op) => isNonJsonSerializable(op.input),true: httpLink({url,}),false: httpBatchLink({url,}),}),],});
tsimport {httpBatchLink,httpLink,isNonJsonSerializable,splitLink,} from '@trpc/client';trpc.createClient({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 base:
tsimport {httpBatchLink,httpLink,isNonJsonSerializable,splitLink,} from '@trpc/client';import superjson from 'superjson';trpc.createClient({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: superjson.deserialize, // or your other transformer},}),false: httpBatchLink({url,transformers: superjson, // or your other transformer}),}),],});
tsimport {httpBatchLink,httpLink,isNonJsonSerializable,splitLink,} from '@trpc/client';import superjson from 'superjson';trpc.createClient({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: superjson.deserialize, // or your other transformer},}),false: httpBatchLink({url,transformers: superjson, // or your other transformer}),}),],});
Server Usage
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 express// incorrectconst app = express();app.use(express.json()); // this try to parse body before tRPC.app.post('/express/hello', (req,res) => {/* ... */ }); // normal express route handlerapp.use('/trpc', trpcExpress.createExpressMiddleware({ /* ... */}))// tRPC fails to parse body// correctconst app = express();app.use('/express', express.json()); // do it only in "/express/*" pathapp.post('/express/hello', (req,res) => {/* ... */ });app.use('/trpc', trpcExpress.createExpressMiddleware({ /* ... */}))// tRPC can parse body
ts// Example in express// incorrectconst app = express();app.use(express.json()); // this try to parse body before tRPC.app.post('/express/hello', (req,res) => {/* ... */ }); // normal express route handlerapp.use('/trpc', trpcExpress.createExpressMiddleware({ /* ... */}))// tRPC fails to parse body// correctconst app = express();app.use('/express', express.json()); // do it only in "/express/*" pathapp.post('/express/hello', (req,res) => {/* ... */ });app.use('/trpc', trpcExpress.createExpressMiddleware({ /* ... */}))// 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,};}),});