Skip to content
tRPC

Partners

Data Transformers

On this page

You are able to serialize the response data & input args. The transformers need to be added both to the server and the client.

Using superjson

SuperJSON allows us to transparently use, e.g., standard Date/Map/Sets over the wire between the server and client. That is, you can return any of these types from your API-resolver and use them in the client without having to recreate the objects from JSON.

How to

1. Install

pnpm add superjson
npm install superjson
yarn add superjson
bun add superjson

2. Add to your initTRPC

server/routers/_app.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import class superjsonsuperjson from 'superjson';

export const 
const t: TRPCRootObject<object, object, {
    transformer: typeof superjson;
}, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<{
    transformer: typeof superjson;
}>(opts?: {
    transformer: typeof superjson;
} | undefined): TRPCRootObject<object, object, {
    transformer: typeof superjson;
}, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>
create
({
transformer: typeof superjsontransformer: class superjsonsuperjson, });

TypeScript will guide you to where you need to add transformer as soon as you've added it on the initTRPC-object

createTRPCClient():

src/app/_trpc/client.ts
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
import class superjsonsuperjson from 'superjson'; export const
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
client
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
httpLink
({
url: string | URLurl: 'http://localhost:3000', transformer: DataTransformerOptionstransformer: class superjsonsuperjson, }), ], });

Every terminating link needs the transformer, including both branches of a splitLink. With wsLink, it goes on wsLink(), not createWSClient().

src/app/_trpc/client.ts
import {
  function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
  function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink,
  function httpSubscriptionLink<TInferrable extends InferrableClientTypes, TEventSource extends EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<inferClientTypes<TInferrable>, TEventSource>): TRPCLink<TInferrable>httpSubscriptionLink,
  
function splitLink<TRouter extends AnyRouter = AnyRouter>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<TRouter> | TRPCLink<TRouter>[];
    false: TRPCLink<TRouter> | TRPCLink<TRouter>[];
}): TRPCLink<TRouter>
splitLink
,
} from '@trpc/client'; import type {
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
import class superjsonsuperjson from 'superjson'; export const
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
client
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
splitLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: {
    condition: (op: Operation) => boolean;
    true: TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<...>>>[];
    false: TRPCLink<...> | TRPCLink<...>[];
}): TRPCLink<...>
splitLink
({
condition: (op: Operation) => booleancondition: (op: Operationop) => op: Operationop.type: "query" | "mutation" | "subscription"type === 'subscription',
true: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
true
:
httpSubscriptionLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>, EventSourceLike.AnyConstructor>(opts: HTTPSubscriptionLinkOptions<PickTypes<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>, EventSourceLike.AnyConstructor>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<...>>>
httpSubscriptionLink
({
UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: 'http://localhost:3000', transformer: DataTransformerOptionstransformer: class superjsonsuperjson, }),
false: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>> | TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
false
:
httpBatchLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink
({
url: string | URLurl: 'http://localhost:3000', transformer: DataTransformerOptionstransformer: class superjsonsuperjson, }), }), ], });

Now a Date returned by a procedure is a Date on the client:

src/usage.ts
const 
const event: {
    name: string;
    at: Date;
}
event
= await
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    event: QueryProcedure<{
        input: void;
        output: {
            name: string;
            at: Date;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>>
client
.
event: {
    query: Resolver<{
        input: void;
        output: {
            name: string;
            at: Date;
        };
        errorShape: DefaultErrorShape;
        transformer: true;
    }>;
}
event
.
query: (input: void, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<{
    name: string;
    at: Date;
}, DefaultErrorShape>
query
();
const event: {
    name: string;
    at: Date;
}
event
.at.Date.getFullYear(): numbergetFullYear();
at: Date

Using devalue

Devalue works like superjson, focusing on performance and compact payloads, but at the cost of a less human-readable body.

How to

1. Install

pnpm add devalue
npm install devalue
yarn add devalue
bun add devalue

2. Add to utils/trpc.ts

Here we use parse and stringify as they mitigate XSS.

utils/trpc.ts
import { function parse(str: string): anyparse, function stringify(value: any): stringstringify } from 'devalue';

// [...]

export const 
const transformer: {
    deserialize: (object: any) => any;
    serialize: (object: any) => string;
}
transformer
= {
deserialize: (object: any) => anydeserialize: (object: anyobject: any) => function parse(str: string): anyparse(object: anyobject), serialize: (object: any) => stringserialize: (object: anyobject: any) => function stringify(value: any): stringstringify(object: anyobject), };

3. Add to your initTRPC

server/routers/_app.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import { 
const transformer: {
    deserialize: (object: any) => any;
    serialize: (object: any) => any;
}
transformer
} from '../../utils/trpc';
export const
const t: TRPCRootObject<object, object, {
    transformer: {
        deserialize: (object: any) => any;
        serialize: (object: any) => any;
    };
}, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<{
    transformer: {
        deserialize: (object: any) => any;
        serialize: (object: any) => any;
    };
}>(opts?: {
    transformer: {
        deserialize: (object: any) => any;
        serialize: (object: any) => any;
    };
} | undefined): TRPCRootObject<object, object, {
    transformer: {
        deserialize: (object: any) => any;
        serialize: (object: any) => any;
    };
}, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>
create
({
transformer: {
    deserialize: (object: any) => any;
    serialize: (object: any) => any;
}
transformer
,
});

TypeScript will guide you to where you need to add transformer as soon as you've added it on the initTRPC-object

createTRPCClient():

src/app/_trpc/client.ts
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpLink<TRouter extends AnyRouter = AnyRouter>(opts: HTTPLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server/routers/_app';
import {
const transformer: {
    deserialize: (object: any) => any;
    serialize: (object: any) => any;
}
transformer
} from './utils/trpc';
export const
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
client
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
httpLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
httpLink
({
url: string | URLurl: 'http://localhost:3000', transformer: DataTransformerOptionstransformer, }), ], });

Different transformers for upload and download

If a transformer should only be used for one direction or different transformers should be used for upload and download (e.g., for performance reasons), you can provide individual transformers for upload and download. Make sure you use the same combined transformer everywhere.

DataTransformer interface

export interface DataTransformer {
  DataTransformer.serialize(object: any): anyserialize(object: anyobject: any): any;
  DataTransformer.deserialize(object: any): anydeserialize(object: anyobject: any): any;
}

interface InputDataTransformer extends DataTransformer {
  /**
   * This function runs **on the client** before sending the data to the server.
   */
  InputDataTransformer.serialize(object: any): anyserialize(object: anyobject: any): any;
  /**
   * This function runs **on the server** to transform the data before it is passed to the resolver
   */
  InputDataTransformer.deserialize(object: any): anydeserialize(object: anyobject: any): any;
}

interface OutputDataTransformer extends DataTransformer {
  /**
   * This function runs **on the server** before sending the data to the client.
   */
  OutputDataTransformer.serialize(object: any): anyserialize(object: anyobject: any): any;
  /**
   * This function runs **only on the client** to transform the data sent from the server.
   */
  OutputDataTransformer.deserialize(object: any): anydeserialize(object: anyobject: any): any;
}

export interface CombinedDataTransformer {
  /**
   * Specify how the data sent from the client to the server should be transformed.
   */
  CombinedDataTransformer.input: InputDataTransformerinput: InputDataTransformer;
  /**
   * Specify how the data sent from the server to the client should be transformed.
   */
  CombinedDataTransformer.output: OutputDataTransformeroutput: OutputDataTransformer;
}

Common mistakes

  • A transformer on only one side, or on only some links. As long as the client is typed with your AppRouter, TypeScript catches this: a link without transformer fails with Property 'transformer' is missing, and a transformer on the client when the server has none fails with You must define a transformer on your `initTRPC`-object first. If the types are bypassed, the mismatch shows up at runtime instead: data arrives still encoded (superjson's { json, meta } wrapper), or calls fail with Unable to transform response from server.
  • Creating a link outside the links array. A link created on its own, for example inside a custom link, isn't checked against your router, so a missing transformer isn't reported. Pass the router type, as in httpBatchLink<AppRouter>({ url, transformer }), to get the check back.
  • Passing transformer to createTRPCClient(). TypeScript rejects it with The transformer property has moved to httpLink/httpBatchLink/wsLink.
  • Forgetting to transform data you hydrate yourself. The links only transform tRPC requests and responses. If you dehydrate a TanStack Query cache on the server and hydrate it on the client, also set serializeData and deserializeData, as shown in the Query Client factory.