@trpc/server
Version:
256 lines (194 loc) • 10.5 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 are sent as POST, so they only work with mutations unless httpLink sets methodOverride to 'POST', and they 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.25.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: 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](https://trpc.io/docs/rpc#overriding-the-default-http-method).
- **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`](https://trpc.io/docs/server/non-json-content-types#client-setup). Only `httpLink` sends non-JSON inputs:
| Link | `application/json` | `FormData` | `File` / `Blob` / `Uint8Array` |
| --------------------------------------------------------------- | :----------------: | :---------: | :----------------------------: |
| [`httpLink`](https://trpc.io/docs/client/links/httpLink) | ✅ | ✅ (`POST`) | ✅ (`POST`) |
| [`httpBatchLink`](https://trpc.io/docs/client/links/httpBatchLink) | ✅ | ❌ | ❌ |
| [`httpBatchStreamLink`](https://trpc.io/docs/client/links/httpBatchStreamLink) | ✅ | ❌ | ❌ |
| [`wsLink`](https://trpc.io/docs/server/websockets) | ✅ | ❌ | ❌ |
With a binary `experimental_encoder` such as MessagePack, `wsLink` can carry `Uint8Array` values, as in the [WebSocket encoder example](https://github.com/trpc/trpc/tree/main/examples/next-websockets-encoder).
- **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`