Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

Files

File and Blob values in inputs and outputs, sent as multipart, raw bodies or streamed chunks.

On this page

File and Blob values can sit anywhere in an input. httpLink sends that call alone (outside a batch) as multipart/form-data, and the handler rebuilds them before validation. maxBodySize covers the whole body.

import { import zz } from 'zod';

const 
const setCover: Procedure<{
    type: "mutation";
    ctx: Context;
    meta: {};
    input: {
        postId: string;
        cover: File;
    };
    output: void;
    errors: SchemaValidationError<z.ZodObject<{
        postId: z.ZodString;
        cover: z.ZodInstanceOf<File>;
    }, z.core.$strip>>;
    services: never;
}>
setCover
= const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure
.
ProcedureBuilder<RootDef<Context, {}>>.input<z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<Context, {}>, z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>> (+1 overload)
input
(import zz.
function object<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}>(shape?: {
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>
object
({ postId: z.ZodStringpostId: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(), cover: z.ZodInstanceOf<File>cover: import zz.
instanceof<{
    new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
    prototype: File;
}>(cls: {
    new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
    prototype: File;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodInstanceOf<File>
export instanceof
instanceof
(
var File: {
    new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
    prototype: File;
}
File
) }))
.
ProcedureBuilder<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.mutation<Promise<void>>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>) => Promise<void>): Procedure<{
    type: "mutation";
    ctx: Context;
    meta: {};
    input: {
        postId: string;
        cover: File;
    };
    output: void;
    errors: SchemaValidationError<z.ZodObject<{
        postId: z.ZodString;
        cover: z.ZodInstanceOf<File>;
    }, z.core.$strip>>;
    services: never;
}>
mutation
((
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts
) =>
const storage: {
    put(id: string, file: File): Promise<void>;
}
storage
.function put(id: string, file: File): Promise<void>put(
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts
.
ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.input: {
    postId: string;
    cover: File;
}
input
.postId: stringpostId,
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    postId: z.ZodString;
    cover: z.ZodInstanceOf<File>;
}, z.core.$strip>>>
opts
.
ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ postId: ZodString; cover: ZodInstanceOf<File>; }, $strip>>>.input: {
    postId: string;
    cover: File;
}
input
.cover: Filecover));
await 
const client: TrpcClient<{
    setCover: Procedure<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            postId: string;
            cover: File;
        };
        output: void;
        errors: SchemaValidationError<z.ZodObject<{
            postId: z.ZodString;
            cover: z.ZodInstanceOf<File>;
        }, z.core.$strip>>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>
client
.
setCover: {
    mutate(input: {
        postId: string;
        cover: File;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<undefined, CallError<{
        type: "mutation";
        ctx: Context;
        meta: {};
        input: {
            postId: string;
            cover: File;
        };
        output: void;
        errors: SchemaValidationError<z.ZodObject<{
            postId: z.ZodString;
            cover: z.ZodInstanceOf<File>;
        }, z.core.$strip>>;
        services: never;
    }, JsonForm, never>>;
}
setCover
.
function mutate(input: {
    postId: string;
    cover: File;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<undefined, CallError<{
    type: "mutation";
    ctx: Context;
    meta: {};
    input: {
        postId: string;
        cover: File;
    };
    output: void;
    errors: SchemaValidationError<z.ZodObject<{
        postId: z.ZodString;
        cover: z.ZodInstanceOf<File>;
    }, z.core.$strip>>;
    services: never;
}, JsonForm, never>>
mutate
({ postId: stringpostId: '1', cover: Filecover: const file: Filefile });

The handler only takes a multipart body with the trpc-version header, which httpLink sends and a cross-site form can't, so a form on another site can't run your mutations (other POST bodies must be application/json for the same reason).

wsLink doesn't send files; the call fails with a BAD_REQUEST. For large uploads, use presigned URLs.

Files in outputs

Outputs can hold files too, over every link:

import { import zz } from 'zod';

const 
const report: Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: File;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: never;
}>
report
= const t: TrpcRoot<Context, {}, RootDef<Context, {}>>t.TrpcRoot<Context, {}, RootDef<Context, {}>>.procedure: ProcedureBuilder<RootDef<Context, {}>>procedure
.
ProcedureBuilder<RootDef<Context, {}>>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input
(import zz.
function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>
object
({ id: z.ZodStringid: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() }))
.
ProcedureBuilder<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.query<File>(resolver: (opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>>) => File): Procedure<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: File;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: never;
}>
query
(
(
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>>
opts
) =>
new var File: new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) => FileFile([function csv(id: string): stringcsv(
opts: ResolverOpts<InputDef<RootDef<Context, {}>, z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>>
opts
.
ResolverOpts<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>>.input: {
    id: string;
}
input
.id: stringid)], 'report.csv', { BlobPropertyBag.type?: string | undefinedtype: 'text/csv' }),
);
const const report: Filereport = await 
const client: TrpcClient<{
    report: Procedure<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: File;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }>;
}, {}, undefined, LinksTypes<readonly [TrpcLink<{
    form: JsonForm;
    provides: "response";
}, any>]>>
client
.
report: {
    query(input: {
        id: string;
    }, opts?: CallOptions<{}> | undefined): TrpcPromise<File, CallError<{
        type: "query";
        ctx: Context;
        meta: {};
        input: {
            id: string;
        };
        output: File;
        errors: SchemaValidationError<z.ZodObject<{
            id: z.ZodString;
        }, z.core.$strip>>;
        services: never;
    }, JsonForm, never>>;
}
report
.
function query(input: {
    id: string;
}, opts?: CallOptions<{}> | undefined): TrpcPromise<File, CallError<{
    type: "query";
    ctx: Context;
    meta: {};
    input: {
        id: string;
    };
    output: File;
    errors: SchemaValidationError<z.ZodObject<{
        id: z.ZodString;
    }, z.core.$strip>>;
    services: never;
}, JsonForm, never>>
query
({ id: stringid }); // File
  • When a File or Blob is the whole output of a call that isn't batched, httpLink gets it as the response body, with its content-type and a content-disposition naming the file, so the URL works as a download link too.
  • Anywhere else (nested in an object, yielded by an async iterable, or in a batch), its bytes stream after the rest of the output as base64 chunks. The value holding it resolves once they've arrived, so a nested File is a File, not a promise. A file that appears twice is sent once.

Binary frames

Those chunks are base64 in JSONL, SSE and WebSocket messages. httpLink({ binary: true }) asks for binary frames instead (application/vnd.trpc.frames: the same lines, length-prefixed, with the bytes unencoded), a third smaller; servers answer with JSONL to clients that don't ask. wsLink({ binary: true }) gets each chunk of bytes as a binary message. localLink always passes the bytes as they are.

Serializers

A root File or Blob output of an unbatched HTTP call is the response body, with any serializer. On a batching chain, plain JSON can't batch it, so such calls take { batch: false } (a type error otherwise). Files anywhere else, and files in inputs, need danSON; the other serializers fail before sending (Serializers).

The examples/media-pipeline app uploads photos as multipart, returns files as response bodies and inside streamed outputs, and reads them as binary frames.