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).
ts
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}` };``
}),
});
Non-JSON Content Types
In addition to JSON, 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 of the box — if you're only using this link, your existing setup should work immediately.
ts
import { 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.
ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const url = '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:
ts
import {
createTRPCClient,
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server';
const url = 'http://localhost:2022';
createTRPCClient<AppRouter>({
links: [
`splitLink({`
`condition: (op) => isNonJsonSerializable(op.input),`
`true: httpLink({`
`url,`
`transformer: {`
`// request - convert data before sending to the tRPC server`
`serialize: (data) => data,`
`// response - convert the tRPC response before using it in client`
`deserialize: (data) => superjson.deserialize(data), // or your other transformer`
`},`
`}),`
`false: httpBatchLink({`
`url,`
`transformer: superjson, // or your other transformer`
`}),`
`}),`
],
});
Server Setup
info
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
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './router';
// incorrect
const app1 = express();
app1.use(express.json()); // this tries to parse body before tRPC.
app1.post('/express/hello', (req, res) => { res.end(); }); // normal express route handler
app1.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter })); // tRPC fails to parse body
// correct
const app2 = express();
app2.use('/express', express.json()); // do it only in "/express/*" path
app2.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.
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)).mutation((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).mutation((opts) => {
`const data = opts.input;`
`const data: ReadableStream<any>`
`return {`
`valid: true,`
`};`
}),
});