UNPKG

@trpc/server

Version:

The tRPC server library

266 lines (211 loc) • 6.45 kB
--- 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. FormData and binary inputs only work with mutations (POST). type: core library: trpc library_version: '11.15.1' requires: - server-setup - links sources: - 'trpc/trpc:www/docs/server/non-json-content-types.md' - 'trpc/trpc:examples/next-formdata/' --- # tRPC -- Non-JSON Content Types ## Setup Server: ```ts // server/trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` ```ts // server/appRouter.ts import { octetInputParser } from '@trpc/server/http'; import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ uploadForm: publicProcedure .input(z.instanceof(FormData)) .mutation(({ input }) => { const name = input.get('name'); return { greeting: `Hello ${name}` }; }), uploadFile: publicProcedure.input(octetInputParser).mutation(({ input }) => { // input is a ReadableStream return { valid: true }; }), }); export type AppRouter = typeof appRouter; ``` Client: ```ts // client/index.ts import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink, } from '@trpc/client'; import type { AppRouter } from '../server/appRouter'; const url = 'http://localhost:3000'; const trpc = createTRPCClient<AppRouter>({ links: [ splitLink({ condition: (op) => isNonJsonSerializable(op.input), true: httpLink({ url }), false: httpBatchLink({ url }), }), ], }); ``` ## Core Patterns ### FormData mutation ```ts // server/appRouter.ts import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ createPost: publicProcedure .input(z.instanceof(FormData)) .mutation(({ input }) => { const title = input.get('title') as string; const body = input.get('body') as string; return { id: '1', title, body }; }), }); ``` ```ts // client usage const form = new FormData(); form.append('title', 'Hello'); form.append('body', 'World'); const result = await trpc.createPost.mutate(form); ``` ### Binary file upload with octetInputParser ```ts // server/appRouter.ts import { octetInputParser } from '@trpc/server/http'; import { publicProcedure, router } from './trpc'; export const appRouter = router({ upload: publicProcedure .input(octetInputParser) .mutation(async ({ input }) => { const reader = input.getReader(); let totalBytes = 0; while (true) { const { done, value } = await reader.read(); if (done) break; totalBytes += value.byteLength; } return { totalBytes }; }), }); ``` ```ts // client usage const file = new File(['hello world'], 'test.txt', { type: 'text/plain' }); const result = await trpc.upload.mutate(file); ``` `octetInputParser` converts Blob, Uint8Array, and File inputs to a ReadableStream on the server. ### Client splitLink with superjson transformer ```ts import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink, } from '@trpc/client'; import superjson from 'superjson'; import type { AppRouter } from '../server/appRouter'; const url = 'http://localhost:3000'; const trpc = createTRPCClient<AppRouter>({ links: [ splitLink({ condition: (op) => isNonJsonSerializable(op.input), true: httpLink({ url, transformer: { serialize: (data) => data, deserialize: (data) => superjson.deserialize(data), }, }), false: httpBatchLink({ url, transformer: superjson, }), }), ], }); ``` When using a transformer, the non-JSON httpLink needs a custom transformer that skips serialization for the request (FormData/binary cannot be transformed) but deserializes the response. ## Common Mistakes ### [HIGH] Using httpBatchLink for FormData requests Wrong: ```ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/appRouter'; const trpc = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: 'http://localhost:3000' })], }); ``` Correct: ```ts import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink, } from '@trpc/client'; import type { AppRouter } from '../server/appRouter'; const url = 'http://localhost:3000'; const trpc = createTRPCClient<AppRouter>({ links: [ splitLink({ condition: (op) => isNonJsonSerializable(op.input), true: httpLink({ url }), false: httpBatchLink({ url }), }), ], }); ``` FormData and binary inputs are not batchable; use `splitLink` with `isNonJsonSerializable()` to route them through `httpLink`. Source: www/docs/server/non-json-content-types.md ### [HIGH] Global body parser intercepting FormData before tRPC Wrong: ```ts import * as trpcExpress from '@trpc/server/adapters/express'; import express from 'express'; import { appRouter } from './appRouter'; const app = express(); app.use(express.json()); app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter })); ``` Correct: ```ts import * as trpcExpress from '@trpc/server/adapters/express'; import express from 'express'; import { appRouter } from './appRouter'; const app = express(); app.use('/api', express.json()); app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter })); ``` A global `express.json()` middleware consumes the request body before tRPC can read it; scope body parsing to non-tRPC routes only. Source: www/docs/server/non-json-content-types.md ### [HIGH] FormData only works with mutations FormData and binary inputs are only supported for mutations (POST requests). Using them with `.query()` throws an error because queries use HTTP GET which cannot carry a request body. Source: www/docs/server/non-json-content-types.md ## See Also - `server-setup` -- initTRPC, routers, procedures - `links` -- splitLink configuration for routing non-JSON requests - `validators` -- z.instanceof(FormData) for FormData validation - `adapter-express` -- Express-specific body parser considerations