Learning tRPC - Running the tRPC Server and Client Consumption
Episode 4 of 19

Learning tRPC - Running the tRPC Server and Client Consumption

This episode brings the tRPC API to life through a real server: a standalone or Express server, an adapter for the Next.js App Router, and a tRPC client in React. You will make your first request and experience type-safe autocompletion from the editor.

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

Introduction

The API from episode 3 is still code that is called directly. Now it is time to bring it to life: serving the router through a real server and consuming it from a client. In episode 4 you will set up the server in two ways — standalone/Express and the Next.js App Router — then create a tRPC client in a React frontend.

By the end of this episode, you will make your first request and see type-safe autocompletion first-hand: the editor knows the procedure name, input shape, and response shape without separate documentation.

Setting Up the Server with Express or Standalone

Standalone Server with createHTTPServer

The easiest way for local development is a standalone server:

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

Run it and verify the endpoint with curl:

Testing the server with curl
npx tsx src/server/index.ts
curl "http://localhost:3000/trpc/user.byId?input=%7B%22id%22%3A1%7D"

The default endpoint is /trpc. Input is sent as JSON encoded in the query parameter — that is why the URL above looks complex. The tRPC client handles this encoding automatically.

Express Middleware

If your project already uses Express, use createExpressMiddleware:

Install Express and CORS
npm install express cors
npm install --save-dev @types/express @types/cors
Express server with tRPC
import express from "express";
import cors from "cors";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "./routers";
 
const app = express();
app.use(cors());
app.use(
  "/trpc",
  createExpressMiddleware({
    router: appRouter,
    createContext: () => ({}),
  }),
);
 
app.listen(3000, () => console.log("Server berjalan di port 3000"));

createExpressMiddleware({ router, createContext }) returns a standard Express middleware, so it can still be combined with other routes. Fastify also has a similar adapter called fastifyTRPCPlugin — the pattern is identical.

Setting Up with the Next.js App Router

Route Handler in the App Router

In the Next.js App Router, tRPC is served through a route handler:

app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers";
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => ({}),
  });
 
export { handler as GET, handler as POST };

With this file, every tRPC procedure is available at /api/trpc without writing a route handler per procedure. The dynamic [trpc] folder catches any procedure path.

Creating the Client in a React Frontend

Setting Up React Query and createTRPCReact

For React, install the additional packages:

Install React client packages
npm install @trpc/react-query @tanstack/react-query

Create the client in a separate file:

lib/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import { httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@/server/routers";
 
export const trpc = createTRPCReact<AppRouter>();
 
export const getBaseUrl = () =>
  process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";

Then wrap your application with QueryClientProvider and trpc.Provider:

Provider for React
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { trpc, getBaseUrl } from "./trpc";
 
const queryClient = new QueryClient();
 
const trpcClient = trpc.createClient({
  links: [httpBatchLink({ url: `${getBaseUrl()}/api/trpc` })],
});
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}

trpc.createClient({ links: [httpBatchLink(...)] }) creates a client instance connected to the /api/trpc endpoint.

First Request and Autocompletion

Using the useQuery Hook

Now tRPC procedures can be called like any ordinary hook:

User component
"use client";
 
import { trpc } from "@/lib/trpc";
 
export default function Profile({ id }: { id: number }) {
  const { data, isLoading } = trpc.user.byId.useQuery({ id });
 
  if (isLoading) return <p>Memuat...</p>;
  return <h1>Halo, {data?.nama}</h1>;
}

Notice: trpc.user.byId.useQuery({ id }) is guaranteed to exist because the user.byId procedure is defined on the server. Misspelling a procedure name or sending the wrong input shape produces a TypeScript error in the editor — this is the type-safe autocompletion that is tRPC's main appeal.

For write operations, trpc.user.create.useMutation() is also available with fully typed output — ideal for forms and other data-changing actions.

Warning

Recall the pattern from episode 2: createTRPCProxyClient for non-React code, createTRPCReact for hooks. Do not mix them in the wrong place — the proxy client does not provide the useQuery hook.

Conclusion

Episode 4 connects the two sides of tRPC: the server serving the router through standalone, Express, or the Next.js App Router, and the React client using it through hooks with full autocompletion.

Key takeaways:

  • createHTTPServer for a standalone server; createExpressMiddleware for Express.
  • The Next.js App Router uses fetchRequestHandler in the /api/trpc route handler.
  • createContext provides per-request data, such as a session.
  • Install @trpc/react-query and @tanstack/react-query for React.
  • trpc.Provider wraps the application and connects the query client.
  • The useQuery and useMutation hooks provide type-safe autocompletion.

In the next episode, episode 5, we will discuss input validation, error handling & response types — deepening zod and superstruct validation, getting to know TRPCError and formatError, HTTP status mapping, and fallback default values for responses.

Learning tRPC - Running the tRPC Server and Client Consumption | Learning tRPC