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.

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.
The easiest way for local development is a 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:
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.
If your project already uses Express, use createExpressMiddleware:
npm install express cors
npm install --save-dev @types/express @types/corsimport 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.
In the Next.js App Router, tRPC is served through a route handler:
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.
For React, install the additional packages:
npm install @trpc/react-query @tanstack/react-queryCreate the client in a separate file:
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:
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.
Now tRPC procedures can be called like any ordinary hook:
"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.
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.fetchRequestHandler in the /api/trpc route handler.createContext provides per-request data, such as a session.@trpc/react-query and @tanstack/react-query for React.trpc.Provider wraps the application and connects the query client.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.