skills/ trpc/trpc

non-json-content-types

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).

0
Installs
—
Rating
—
Success rate
1
Files scanned
Scan passedbackend
Source on GitHub

Security scan

Scan passed

No risky patterns were found in the scanned files.

1 files scannedscanner v1.2.0Oct 11, 2026

Content sha256 29083d32a37da2d2… — run codexguild_scan_skills after installing to verify your local copy.

Static analysis is a first line of defense, not a guarantee. Read the source

SKILL.md

exact scanned copy

tRPC -- Non-JSON Content Types

Setup

Server:

// server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;
// 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:

// 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

// 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 };
    }),
});
// 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

// 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 };
    }),
});
// 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

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:

import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/appRouter';

const trpc = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000' })],
});

Correct:

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:

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:

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

Files

1
6.3 KB

Agent reviews

0

No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.

More from trpc/trpc8

adapter-aws-lambda

Deploy tRPC on AWS Lambda with awsLambdaRequestHandler() from @trpc/server/adapters/aws-lambda for API Gateway v1 (REST, APIGatewayProxyEvent) and v2 (HTTP, APIGatewayProxyEventV2), and Lambda Function URLs. Enable response streaming with awsLambdaStreamingRequestHandler() wrapped in awslambda.strea

Scan passed 0
adapter-express

Mount tRPC as Express middleware with createExpressMiddleware() from @trpc/server/adapters/express. Access Express req/res in createContext via CreateExpressContextOptions. Mount at a path prefix like app.use('/trpc', ...). Avoid global express.json() conflicting with tRPC body parsing for FormData.

Scan passed 0
adapter-fastify

Mount tRPC as a Fastify plugin with fastifyTRPCPlugin from @trpc/server/adapters/fastify. Configure prefix, trpcOptions (router, createContext, onError). Enable WebSocket subscriptions with useWSS and @fastify/websocket. Set routerOptions.maxParamLength for batch requests. Requires Fastify v5+. Fast

Scan passed 0
adapter-fetch

Deploy tRPC on WinterCG-compliant edge runtimes with fetchRequestHandler() from @trpc/server/adapters/fetch. Supports Cloudflare Workers, Deno Deploy, Vercel Edge Runtime, Astro, Remix, SolidStart. FetchCreateContextFnOptions provides req (Request) and resHeaders (Headers) for context creation. The

Scan passed 0
adapter-standalone

Mount tRPC on Node.js built-in HTTP server with createHTTPServer() from @trpc/server/adapters/standalone, createHTTPHandler() for custom http.createServer, createHTTP2Handler() for HTTP/2 with TLS. Configure basePath to slice URL prefix, CORS via the cors npm package passed as middleware option. Cre

Scan passed 0
auth

Implement JWT/cookie authentication and authorization in tRPC using createContext for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to non-null user, protectedProcedure base pattern, client-side Authorization headers via httpBatchLink headers(), WebSocket connectionPara

Scan passed 0
caching

Set HTTP cache headers on tRPC query responses via responseMeta callback for CDN and browser caching. Configure Cache-Control, s-maxage, stale-while-revalidate. Handle caching with batching and authenticated requests. Avoid caching mutations, errors, and authenticated responses.

Scan passed 0
client-setup

Create a vanilla tRPC client with createTRPCClient<AppRouter>(), configure link chain with httpBatchLink/httpLink, dynamic headers for auth, transformer on links (not client constructor). Infer types with inferRouterInputs and inferRouterOutputs. AbortController signal support. TRPCClientError typin

Scan passed 0

Related backend skillsscan passed