@trpc/server
Version:
247 lines (187 loc) • 8.89 kB
Markdown
---
name: non-json-content-types
description: Handle FormData, file uploads, Blob, Uint8Array, and ReadableStream inputs in tRPC mutations. Use octetInputParser from @trpc/server/http for binary data. Route non-JSON requests with splitLink and isNonJsonSerializable() from @trpc/client, since batching links send them as `{}`. FormData and binary inputs only work with mutations (POST) and skip the transformer. Keep Express or Next.js body parsers off tRPC routes.
requires:
- server-setup
- links
sources:
- www/v11/docs/server/non-json-content-types.md
- www/v11/docs/server/adapters/express.md
metadata:
type: core
library: trpc
library_version: 11.23.0
---
<!-- Generated from www/v11/docs/server/non-json-content-types.md by www/build/skills.ts. Edit the docs instead. -->
# Content Types
From https://trpc.io/docs/server/non-json-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`).
```ts no-check
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
> [!NOTE] 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 no-check
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 no-check
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 no-check
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
> [!NOTE] 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:
>
> - Express: see [Body parsers](https://trpc.io/docs/server/adapters/express#body-parsers).
> - Next.js Pages Router: turn off Next's body parser in the tRPC API route with `export const config = { api: { bodyParser: false } }`, as in our [FormData example](https://github.com/trpc/trpc/tree/main/examples/next-formdata).
#### `FormData` Input
FormData is natively supported, and for more advanced usage you could also combine this with a library like [zod-form-data](https://www.npmjs.com/package/zod-form-data) to validate inputs in a type-safe way.
```ts no-check
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;
return {
greeting: `Hello ${data.get('name')}`,
};
}),
});
```
On the client, pass a `FormData` instance to the mutation:
```ts no-check
const form = new FormData();
form.append('name', 'Bilbo');
const result = await client.hello.mutate(form);
```
For a more advanced code sample you can see our [example project here](https://github.com/juliusmarminge/trpc-interop/blob/66aa760141030ffc421cae1a3bda9b5f1ab340b6/src/server.ts#L28-L43)
#### `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 no-check
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(async (opts) => {
const data = opts.input;
const reader = data.getReader();
let bytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.byteLength;
}
return { bytes };
}),
});
```
On the client, pass a `File`, `Blob` or `Uint8Array` to the mutation:
```ts no-check
const file = new File(['hello world'], 'hello.txt', { type: 'text/plain' });
await client.upload.mutate(file);
```
## Common mistakes
- **Using a query.** FormData and binary inputs are sent as the body of a `POST` request, so they only work with mutations: `httpLink` throws `FormData is only supported for mutations` for a query.
- **Sending them through a batching link.** `httpBatchLink`, `httpBatchStreamLink` and `wsLink` serialize inputs as JSON, which turns a `FormData` or `File` into `{}`, so input validation fails. Route them to `httpLink` with a [`splitLink`](https://trpc.io/docs/server/non-json-content-types#client-setup).
- **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](https://trpc.io/docs/server/non-json-content-types#client-setup).
- **Letting your server parse the body first.** See [Server Setup](https://trpc.io/docs/server/non-json-content-types#server-setup).
## Body parsers
tRPC parses request bodies itself, so tRPC routes don't need `express.json()`. If a body parser runs before tRPC anyway, tRPC uses the `req.body` it parsed, which works for JSON. For [FormData and binary inputs](https://trpc.io/docs/server/non-json-content-types) it depends on your Express version:
- **Express 5**: `express.json()` and `express.urlencoded()` leave other content types unread, so tRPC still reads them.
- **Express 4**: the parsers set `req.body` to `{}` on content types they skip, so tRPC receives `{}` instead of the form data or file.
- In both versions, a parser that reads the body you upload, like `express.raw()` for `application/octet-stream`, breaks binary inputs.
To be safe, register body parsers only on the routes that need them:
```ts no-check title="server.ts"
import * as trpcExpress from '@trpc/server/adapters/express';
import express from 'express';
import { appRouter } from './router';
const app = express();
app.use('/api', express.json()); // only for your own `/api/*` routes
app.post('/api/webhook', (req, res) => {
res.json({ received: req.body });
});
app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter }));
app.listen(4000);
```
From https://trpc.io/docs/server/adapters/express#body-parsers
## Related skills
- `server-setup` (https://trpc.io/docs/server/overview): `npx @tanstack/intent@latest load @trpc/server#server-setup`
- `links` (https://trpc.io/docs/client/links): `npx @tanstack/intent@latest load @trpc/client#links`