Learning tRPC - Transport, HTTP, and WebSocket
Episode 10 of 19

Learning tRPC - Transport, HTTP, and WebSocket

This episode breaks down the tRPC transport layer: HTTP batching with httpBatchLink, standard HTTP with httpLink, and WebSocket with wsLink. You will also learn CORS configuration, request headers, and setting up server subscriptions in Express.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

All of tRPC's type-safety eventually passes through the network. Episode 10 breaks down transport — the layer that determines how requests are sent. You will understand the differences between httpLink, httpBatchLink, and wsLink, when to use each, as well as supporting configuration such as CORS, request headers, and proxy.

Choosing the right transport has a direct impact on latency and user experience, so understand the characteristics of each link well.

HTTP Batch vs Standard HTTP

httpLink sends every operation as a separate HTTP request. Suitable for mutations and applications with few queries:

A simple httpLink
import { createTRPCClient, httpLink } from "@trpc/client";
 
const client = createTRPCClient<AppRouter>({
  links: [httpLink({ url: "http://localhost:3000/trpc" })],
});
 
const user = await client.user.byId.query({ id: 1 });

httpLink({ url }) sends the operation with a POST method to the specified URL. Because one operation equals one request, this model is simple and suitable when batching offers little benefit.

httpBatchLink combines many operations that happen at the same time into one request. Queries called by several components at once will be sent together:

httpBatchLink on the client
import { httpBatchLink } from "@trpc/client";
 
const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({ url: "http://localhost:3000/trpc" }),
  ],
});
 
const [user, posts] = await Promise.all([
  client.user.byId.query({ id: 1 }),
  client.post.list.query({ authorId: 1 }),
]);

With httpBatchLink, the two queries above run in a single HTTP round-trip — the server processes both and returns the results in one response. This is a huge latency saving in applications with many queries per page. An important note: mutations must not be batched. If necessary, use splitLink to route mutations to httpLink.

WebSocket and Subscriptions

Subscriptions require a persistent connection. wsLink maintains a WebSocket connection and streams real-time events:

wsLink for subscriptions
import { wsLink } from "@trpc/client";
 
const client = createTRPCClient<AppRouter>({
  links: [
    wsLink({
      url: "ws://localhost:3000/trpc",
    }),
  ],
});
 
const unsub = client.clock.subscribe(undefined, {
  onData: (data) => console.log("Waktu baru:", data.waktu),
});

client.clock.subscribe(undefined, { onData }) opens a stream and calls onData every time the server sends an event. Because the WebSocket stays open, this is ideal for notifications, chat, and price updates.

Combining All Transports

Use splitLink so a single client handles all three — exactly the pattern from episode 6:

Combined splitLink
links: [
  splitLink({
    condition: (op) => op.type === "subscription",
    true: wsLink({ url: "ws://localhost:3000" }),
    false: httpBatchLink({ url: "http://localhost:3000/trpc" }),
  }),
],

Subscriptions through WebSocket, while queries and mutations through HTTP batch — the most common combination in production.

Setting Up Subscriptions on the Server

WebSocket Server with Express

On the server side, WebSocket is served alongside HTTP. Use wsServer (called applyWSSHandler in tRPC v10), which imports the same router:

tRPC WebSocket server
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { wsServer } from "@trpc/server/adapters/ws";
import { appRouter } from "./routers";
 
const { server } = createHTTPServer({
  router: appRouter,
  createContext: () => ({}),
});
 
wsServer({
  wss: new WebSocketServer({ server }),
  router: appRouter,
  createContext: () => ({}),
});
 
server.listen(3000);

The same router serves both HTTP and WebSocket at once — no duplication of definitions. WebSocketServer is imported from the ws package, a dependency you need to install:

Install WebSocket dependency
npm install ws
npm install --save-dev @types/ws

CORS, Request Headers, and Proxy

CORS Configuration on the Server

When the client and server are on different domains, CORS must be configured. In Express, simply mount the middleware before tRPC:

CORS for tRPC
app.use(cors({ origin: ["http://localhost:5173"], credentials: true }));

If authentication uses cookies, you must enable credentials: true and specify the origin explicitly — wildcards are not supported for cookies.

Injecting Request Headers

The client can add headers via the headers option on a link, or use op.context for dynamic values:

Dynamic per-request headers
import { httpBatchLink } from "@trpc/client";
 
const client = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "/api/trpc",
      headers: () => ({
        Authorization: `Bearer ${getToken()}`,
      }),
    }),
  ],
});

headers: () => ({ Authorization }) adds a token to every request. On the server side, the token is read from ctx.req.headers in the authentication middleware — the flow will be completed in episode 11.

Proxy Behind a Reverse Proxy

tRPC runs normally behind a reverse proxy like Nginx or Traefik as long as the path is routed to the application. What to watch out for: WebSocket requires the upgrade header to be forwarded by the proxy, and CORS is still configured in the application because the proxy preserves headers.

Warning

Do not put auth tokens in the URL query. Use the Authorization header. Query strings are commonly recorded in server and proxy logs — the correct header prevents tokens leaking through logs.

Conclusion

Episode 10 completes the transport layer: httpLink for single requests, httpBatchLink for round-trip savings, wsLink for real-time, as well as CORS, headers, and proxy configuration so the server is ready to be accessed from anywhere.

Key takeaways:

  • httpLink is one request per operation; httpBatchLink combines queries.
  • Mutations must not be batched — route them with splitLink.
  • wsLink is required for real-time subscriptions.
  • wsServer (v10: applyWSSHandler) serves WebSocket with the same router.
  • CORS with credentials: true is required when the client is on a different domain.
  • Auth tokens are sent via headers, not the query string.

In the next episode, episode 11, we will discuss authentication & authorization in tRPC — authentication middleware with tokens, sessions, or cookies, per-procedure authorization and RBAC, as well as integration with NextAuth, Clerk, or a custom auth provider.

Learning tRPC - Transport, HTTP, and WebSocket | Learning tRPC