Back to Trpc

tRPC -- SuperJSON Transformer

packages/client/skills/superjson/SKILL.md

11.16.05.4 KB
Original Source

tRPC -- SuperJSON Transformer

Setup

1. Install superjson

bash
npm install superjson

2. Add to initTRPC on the server

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

const t = initTRPC.create({
  transformer: superjson,
});

export const router = t.router;
export const publicProcedure = t.procedure;
ts
// client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';

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

Now Date, Map, Set, BigInt, RegExp, undefined, and other non-JSON types survive the round trip.

Core Patterns

ts
import {
  createTRPCClient,
  httpBatchLink,
  httpSubscriptionLink,
  splitLink,
} from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';

const client = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: httpSubscriptionLink({
        url: 'http://localhost:3000/trpc',
        transformer: superjson,
      }),
      false: httpBatchLink({
        url: 'http://localhost:3000/trpc',
        transformer: superjson,
      }),
    }),
  ],
});

Every terminating link in every branch must have transformer: superjson.

ts
import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
import superjson from 'superjson';
import type { AppRouter } from './server/trpc';

const wsClient = createWSClient({
  url: 'ws://localhost:3000',
});

const client = createTRPCClient<AppRouter>({
  links: [
    wsLink<AppRouter>({
      client: wsClient,
      transformer: superjson,
    }),
  ],
});

Returning Dates from Procedures

ts
// server
import { z } from 'zod';
import { publicProcedure, router } from './trpc';

const appRouter = router({
  getEvent: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => {
      return {
        id: input.id,
        name: 'Launch Party',
        date: new Date('2025-01-01T00:00:00Z'),
      };
    }),
});

export type AppRouter = typeof appRouter;
ts
// client
const event = await client.getEvent.query({ id: '1' });
console.log(event.date instanceof Date); // true
console.log(event.date.getFullYear()); // 2025

Without superjson, event.date would be a string like "2025-01-01T00:00:00.000Z".

Common Mistakes

Wrong:

ts
// Server
const t = initTRPC.create({ transformer: superjson });

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

Correct:

ts
// Server
const t = initTRPC.create({ transformer: superjson });

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

Server encodes with superjson but client tries to parse raw JSON, causing "Unable to transform response" or garbled data.

Source: www/docs/server/data-transformers.md

The transformer option is on individual terminating links:

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

In v11, transformer was moved from the client constructor to individual links. Passing it to createTRPCClient throws a TypeError.

Source: packages/client/src/internals/TRPCUntypedClient.ts

Wrong:

ts
splitLink({
  condition: (op) => op.type === 'subscription',
  true: httpSubscriptionLink({
    url: 'http://localhost:3000/trpc',
    // missing transformer!
  }),
  false: httpBatchLink({
    url: 'http://localhost:3000/trpc',
    transformer: superjson,
  }),
});

Correct:

ts
splitLink({
  condition: (op) => op.type === 'subscription',
  true: httpSubscriptionLink({
    url: 'http://localhost:3000/trpc',
    transformer: superjson,
  }),
  false: httpBatchLink({
    url: 'http://localhost:3000/trpc',
    transformer: superjson,
  }),
});

Every terminating link must have the same transformer. A missing transformer on one branch causes deserialization failures only for operations routed through that branch.

Source: www/docs/server/data-transformers.md

[HIGH] Using transformer on client but not on server

Wrong:

ts
// Server -- no transformer
const t = initTRPC.create();

// Client
httpBatchLink({ url, transformer: superjson });

Correct:

ts
// Server
const t = initTRPC.create({ transformer: superjson });

// Client
httpBatchLink({ url, transformer: superjson });

The transformer must be configured on both initTRPC.create() and every client link. Client-only transformer corrupts the request encoding because the server expects plain JSON.

Source: www/docs/server/data-transformers.md

See Also

  • client-setup -- create the tRPC client and configure links
  • links -- detailed options for each link type including transformer
  • server-setup -- initTRPC.create() where the server transformer is configured