tRPC
ุชุณู ุญ ูู tRPC ุจูุชุงุจุฉ type safe api ุฏููู ุงูุญูุงุฌุฉ ุฅูู ุชููููุฏ ููุฏ ูุชููุญู ุนูู ุญูุฏูุซ ุฃุฎุทุงุก ู ูุงุฌุฆุฉ ุฃุซูุงุก ุงูู runtimeุ ุญูุซ ุฅููุง ุชูุณุชุบู ุฎุงุตูุฉ ุงูู inference ูู Typescript ุญุชู ุชุถู ู ุงูู type safety ุนููุฏ ูุฏุงุก ุงูู Api ู ู ุงูู Frontend
I built tRPC to allow people to move faster by removing the need of a traditional API-layer, while still having confidence that our apps won't break as we rapidly iterate.
Alex - creator of tRPC @alexdotjs
Files
ูุณูุก ุงูุญุธ ูุฅู tRPC ุชุชุทูุจ ููููุงู ู
ู ุงูู boilerplate ูููู ูุญุณู ุงูุญุธ ูุงู create-t3-app
ุชุญู
ู ุนูู ูุฐุง ุงูุนุจุก.
๐ ู
ูู pages/api/trpc/[trpc].ts
ูุฐุฉ ูู ููุทุฉ ุฏุฎููู ุงูู tRPC Apiุ ูู ุงูุฃูุถุงุน ุงูุทุจูุนูุฉ ูู ุชุญุชุงุฌ ุงูู ุฃู ุชูู
ุณ ูุฐุง ุงูู
ูู ูุซูุฑุง. ููู
ููู ุชุบููุฑู ุนูุฏ ุชูุนูู CORS Middleware ุงู ุดุฆ ู
ู ูุฐุง ุงููุจูู ููููู
ุจุนู
ู export ูู createNextHandler
Next.js API handlerโ ูุงูุฐู ููุจู requestโ ู responseโ
ู
ู
ุง ูุนูู ุฃูู ูุงุฏุฑ ุนูู ุงุณุชุฎุฏุงู
createNextApiHandler
ูู ุฃู middleware ุชุฑูุฏูุ ุฅูุฑุฃ example snippet
ู
ูู ๐ server/trpc/context.ts
ูู ูุฐุง ุงูู ูู ุชููู ุจุงูุดุงุก ุงูู Context ุงูุชู ุณูุชู ุชู ุฑูุฑู ุงูู tRPC Procedure ุ ุงูู Context ูู ุนุจุงุฑุฉ ุนู ุงูุจูุงูุงุช ุงูุชู ุณูููู ููู ุงูู Procedures ูุตูู ููุง ููู ู ูุงู ู ููุงุณุจ ูุชุถุน ุฃุดูุงุก ู ุซู database connections ูู ุนููู ุงุช ุงูู ุตุงุฏูุฉ ูุบูุฑูุง.
- ู
ุง ูู
createContextInner
: ูููุง ุชูููู ุจุฅูุดุงุก ุงูู Context ุงูุฐู ูุง ููุนุชู ุฏ ุนููู ุงูู request ู ูุซู ุฅุชุตุงู ูุงุนุฏุฉ ุงูุจูุงูุงุช. ููู ููู ุฅุณุชุฎุฏุงู function ูู integration testing ุงู ssg-helpersโ - ู
ุง ูู
createContext
ุ ูููุง ุญููุซ ุชูููู ุจุฅูุดุงุก ุงูู Context ุงูุฐู ูุนุชู ุฏ ุนูู ุงูู request ููู ููู ุงููุตูู ุงูู ุงููreq Object
ุนู ุทุฑููopts.req
ูู ู ุซูู ุชูู ุฑูุฑุฉ ุงููcreateContextInner
ูุฅูุดุงุก ุงูู Context ุงูููุงุฆู
๐ู
ูู server/trpc/trpc.ts
ูู ูุฐุง ุญููุซู ูู
ููู ุชุญุฏูุฏ ุงูู proceduresโ ู middlewaresโุ ู
ู ุงูุงูุถู ุงู ูุง ุชููู
ุจุนู
ู export ูู t Object ูุงู
ูุง
/middlewares) ุจู ูู
ุจุชุตุฏูุฑ procedures ู middlewares
ุณุชูุงุญุธ ุฃููุง ูุณุชุฎุฏู
superjson
ูู data transformerโุ ุฐูู ุญุชู ูุญูุธ ุงูู Types ูุญูู ุฅุณุชุฎุฏุงู
ูุง ูู ูู ุงูู clientุ ูู
ุซูุง ุฅุฐุง ูุงู ุงูู Type ูู Date ูุฅู ุงูู client ุณูููุนูุฏ Date ,gds string
๐ ู
ูู server/trpc/router/*.ts
ููุง ูู
ููู ุชุญุฏูุฏ ุงูู route ,ูุงูู procedure ููู APIุ ู
ู ุงูุงูุถู ุฃู ุชููุดุฆ routersโ ู
ูููุตูุฉ ููู procedures ุงูู
ุชูุงุฑุจุฉ ูู
ู ุซูู
ุฏู
ุฌูุงโ ูู router ูุงุญุฏ ูู server/trpc/router/_app.ts
๐ ู
ูู utils/trpc.ts
ูุฐู ูู ููุทุฉ ุฏุฎูู ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ ูู tRPC. ูุฐุง ูู ุงูู ูุงู ุงูุฐู ุณุชููู ููู ุจุงุณุชูุฑุงุฏ type definition ุงูุฎุงุต ุจุงูู procedure ูุฅูุดุงุก tRPC client ุงูุฎุงุต ุจู ุฌูุจูุง ุฅูู ุฌูุจ ู ุน react query hooks. ูุธุฑูุง ูุฃููุง ูู ูุง ุจุชูุนูู โsuperjsonโ ูู ุงููุงุฌูุฉ ุงูุฎูููุฉ ููุญู ุจุญุงุฌุฉ ุฅูู ุชูุนููุฉ ุนูู ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ ุฃูุถูุง. ูุฐุง ูุงู ุงูุจูุงูุงุช ุงูุชู ูุญุฏุซ ููุง serialized ูู ุงูู client ูุชู ุนู ู deserialized ููุง ูู ุงูู client.
ููุง ุชููู
ุจุชุญุฏูุฏ ุฑูุงุจุทโ ุงูู tRPC ุญูุซ ุชููุญุฏุฏ ุงูู
ุณุงุฑ ุงูุฐู ุณูู
ุฑ ุจู ุงูู request ู
ู ุงูู client ุฅูู ุงูู server
ูุญู ูุณุชุฎุฏู
httpBatchLink
โ ุจุดูู ุฅูุชุฑุงุถู ู
ุน ุชูุนูู request batchingโ ู loggerLink
โ
ููู ุงูุงุฎูุฑ ูููู ุจุชุตุฏูุฑ helper typeโ ุญุชู ูุณุชุนู ู ุงูู type infre ูู ุงูู frontend
ููู ุฃุณุชุฎุฏู tRPC ุ
ููุตุญู ุจู ุดุงูุฏุฉ ูุฐุงa killer talk at Next.js Confโ ู ู trashh_devโ
ู ุน tRPCุชูุชุจ Function ูู ุงูู backend ูุงูุชู ูู ูู ู ูุงุฏุงุชูุง ู ู ุงูู frontend
const userRouter = t.router({
getById: t.procedure.input(z.string()).query(({ ctx, input }) => {
return ctx.prisma.user.findFirst({
where: {
id: input,
},
});
}),
});
ูู ููุงูุฉ ุงูุฃู ุฑ ุชุชุญูู tRPC procedure ุงูู backend ุนุงุฏู ููููู ุจูุญุต ุงูู input ููู ุฑุฑ ุงูู request ุฅุฐุง ูุงู ุตุญูุญุงูููุนูุฏ ุฑุณุงูุฉ ุฎุทุฃ ุฅุฐุง ูุงูุช ุงูู ุฏุฎูุงุช ุบูุฑ ุตุญูุญุฉ. ุจุนุฏ ุงูุชุฃูุฏ ู ู ุตุญุฉ ุงูุจูุงูุงุช ูุชู ูุฏุงุก function ูุงูุชู ุฅู ุง ูุฌูุจ ุจูุงูุงุช (queryโ) ุฃู ุฃู ุชุบูุฑ ูู ุงูุจุงูุงุช (mutationโ) ุฃูุช
const appRouter = t.router({
users: userRouter,
posts: postRouter,
messages: messageRouter,
});
export type AppRouter = typeof appRouter;
ูุงุญุธ ุฃููุง ูููู
ุจุนู
ู export ููุท ูู routerโs type ุฃู ุฃููุง ูุง ูุณุชุฎุฏู
ุงู ู
ู ุงูู server code ูู ุงูู client
ุงูุงู ุฏุนูุง ููุงุฏู ุงูู procedure ู
ู ุงูู frontend ุ tRPC ุชููุฑ wrapper ูู
ูุชุจุฉ @tanstack/react-query
ู
ู
ุง ูุณู
ุญ ูู ุจุฅุณุชุฎุฏุงู
ุงูู
ูุชุจุฉ ุจูุงู
ู ููุชูุง.
import { useRouter } from "next/router";
const UserPage = () => {
const { query } = useRouter();
const userQuery = trpc.user.getById.useQuery(query.id);
return (
<div>
<h1>{userQuery.data?.name}</h1>
</div>
);
};
ุณุชูุงุญุธ ุนูู ุงูููุฑ ู
ุฏู ุฌูุฏุฉ ุงูุฅูู
ุงู ุงูุชููุงุฆู ูุงูู typesafety. ุจู
ุฌุฑุฏ ูุชุงุจุฉ โtrpc.โ ุ ุณุชุธูุฑ router
ุงูุฎุงุตุฉ ุจู ูู ุงูุฅูู
ุงู ุงูุชููุงุฆู ุ ูุนูุฏู
ุง ุชุญุฏุฏ ุงูู router
ุ
ุณุชุธูุฑ ุงูู procedures. ูุณุชุญุตู ุฃูุถูุง ุนูู ุฎุทุฃ TypeScript ุฅุฐุง ูุงูุช ุงูู
ูุฏุฎูุงุช ุงูุฎุงุต ุจู ูุง ูุชุทุงุจู ู
ุน ุงูู schema ุงูุฐู ุญุฏุฏุชู ู
ุณุจูุง.
ููู ุงููุงุฏู API ุฎุงุฑุฌู ุ
ุจุงุณุชุฎุฏุงู
ุงูู API ุงูุนุงุฏูุฉ ุ ูู
ููู ุงุณุชุฏุนุงุก ุงูู End point ุงูุฎุงุตุฉ ุจู ุจุงุณุชุฎุฏุงู
ุฃู ุนู
ูู HTTP ู
ุซู curl
ุฃู Postman
ุฃู fetch
ุฃู ู
ุจุงุดุฑุฉ ู
ู ู
ุชุตูุญู.
ู
ุน tRPC ุ ุงูุฃู
ุฑ ู
ุฎุชูู ุจุนุถ ุงูุดูุก. ุฅุฐุง ููุช ุชุฑุบุจ ูู ุงูุงุชุตุงู ุจุงูู procedure ุจุฏูู ุนู
ูู tRPC ุ ูููุงู ุทุฑููุชุงู ู
ูุตู ุจูู
ุง ููููุงู
ุจุฐูู:
Expose a single procedure externally
ุฅุฐุง ุฃุฑุฏุช ุฃู ุชูุชูุญ procedure ููู Apis ุงูุฎุงุฑุฌูุฉ ุงูู ูุธุฑุฉ ุนูู server side callsโุ ู ู ุง ุณูุณู ุญ ูู ุจุนู ู Next.js Api ุฅุนุชูุงุฏูุฉ
import { type NextApiRequest, type NextApiResponse } from "next";
import { appRouter, createCaller } from "../../../server/api/root";
import { createTRPCContext } from "../../../server/api/trpc";
const userByIdHandler = async (req: NextApiRequest, res: NextApiResponse) => {
// Create context and caller
const ctx = await createTRPCContext({ req, res });
const caller = createCaller(ctx);
try {
const { id } = req.query;
const user = await caller.user.getById(id);
res.status(200).json(user);
} catch (cause) {
if (cause instanceof TRPCError) {
// An error from tRPC occurred
const httpCode = getHTTPStatusCodeFromError(cause);
return res.status(httpCode).json(cause);
}
// Another error occurred
console.error(cause);
res.status(500).json({ message: "Internal server error" });
}
};
export default userByIdHandler;
ุชุญูู ูู ุงูู Procedures ุงูู REST endpoint ุ
ุฅุฐุง ููุช ุชุฑุบุจ ูู ูุดู ูู ุงูู ุูุฎุคุซูุนูุซุณ ุ ุงูู ูุธุฑุฉ ุนูู trpc-openapiโ.
Itโs just HTTP Requests
tRPC communicates over HTTP, so it is also possible to call your tRPC procedures using โregularโ HTTP requests. However, the syntax can be cumbersome due to the RPC protocolโ that tRPC uses. If youโre curious, you can check what tRPC requests and responses look like in your browserโs network tab, but we suggest doing this only as an educational exercise and sticking to one of the solutions outlined above.
Comparison to a Next.js API endpoint
ุฏุนูุง ููุงุฑู โ Next.js Endpointโ ุจู โtRPC procedureโ. ูููุชุฑุถ ุฃููุง ูุฑูุฏ ุฌูุจ โObjectโ ู ุณุชุฎุฏู ู ุนูู ู ู ูุงุนุฏุฉ ุจูุงูุงุชูุง ูุฅุนุงุฏุชู ุฅูู ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ. ูู ูููุง ูุชุงุจุฉ Next.js API Endpoint ู ุซู ูุฐุง:
import { type NextApiRequest, type NextApiResponse } from "next";
import { prisma } from "../../../server/db/client";
const userByIdHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "GET") {
return res.status(405).end();
}
const { id } = req.query;
if (!id || typeof id !== "string") {
return res.status(400).json({ error: "Invalid id" });
}
const examples = await prisma.example.findFirst({
where: {
id,
},
});
res.status(200).json(examples);
};
export default userByIdHandler;
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
const UserPage = () => {
const router = useRouter();
const { id } = router.query;
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/user/${id}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [id]);
};
ูุงุฑู ูุฐุง ุจู ุซุงู tRPC ุฃุนูุงู ููู ููู ุฑุคูุฉ ุจุนุถ ู ุฒุงูุง tRPC:
- ุจุฏูุงู ู
ู ุชุญุฏูุฏ ุนููุงู url ููู ู
ุณุงุฑ ุ ูุงูุฐู ูู
ูู ุฃู ูุตุจุญ ู
ุฒุนุฌูุง ุฅุฐุง ุญุงููุช ููู ุดูุก ู
ุง ุ ูุฅู ุงูู
router
ุจุฃูู ูู ุนุจุงุฑุฉ ุนูObject
ู ุน ุงูุฅูู ุงู ุงูุชููุงุฆู. - ูุณุช ุจุญุงุฌุฉ ุฅูู ุงูุชุญูู ู ู HTTP method ุงูุชู ุชู ุงุณุชุฎุฏุงู ูุง.
- ูุง ุชุญุชุงุฌ ุฅูู ุงูุชุญูู ู
ู ุฃู ุงูุทูุจ ุฃู ุงูู
query
ุ ูุฃู Zod ูุนุชูู ุจุฐูู. - ุจุฏูุงู ู ู ุฅูุดุงุก ุงูู responde object ุ ูู ููู ุฅุฑุฌุงุน ุฃุฎุทุงุก ุงู ููู ุฉ ุฃู Object ูู ุง ุชูุนู ูู ุฃู function.
snippets ู ููุฏุฉ
ุชูุนูู CORS
import { type NextApiRequest, type NextApiResponse } from "next";
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "~/server/trpc/router/_app";
import { createContext } from "~/server/trpc/context";
import cors from "nextjs-cors";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Enable cors
await cors(req, res);
// Create and call the tRPC handler
return createNextApiHandler({
router: appRouter,
createContext,
})(req, res);
};
export default handler;
Optimistic updates
ุงูู Optimistic updates ูู ุชุญุฏูุซุงุช ุชุญุฏูุซ ูุงุฌูุฉ ุงูู ุณุชุฎุฏู ูุจู ุฃู ููุชูู ุงูู Request ู ู ุง ููุญุณู ุชุฌุฑุจุฉ ุงูู ุณุชุฎุฏู ุ ููู ุงูุชุทุจููุงุช ุงูุชู ุชููุถู ุฏูุฉ ุงูู ุนููู ุงุช ูุฌุจ ุฃู ุชุชุฌูุจ ุงูู Optimistic updatesุ ููู ุฒูุฏ ู ู ุงูู ุนููู ุงุช ุฅูุฑุง React Query docsโ.
const MyComponent = () => {
const listPostQuery = trpc.post.list.useQuery();
const utils = trpc.useContext();
const postCreate = trpc.post.create.useMutation({
async onMutate(newPost) {
// Cancel outgoing fetches (so they don't overwrite our optimistic update)
await utils.post.list.cancel();
// Get the data from the queryCache
const prevData = utils.post.list.getData();
// Optimistically update the data with our new post
utils.post.list.setData(undefined, (old) => [...old, newPost]);
// Return the previous data so we can revert if something goes wrong
return { prevData };
},
onError(err, newPost, ctx) {
// If the mutation fails, use the context-value from onMutate
utils.post.list.setData(undefined, ctx.prevData);
},
onSettled() {
// Sync with server once mutation has settled
utils.post.list.invalidate();
},
});
};
ุนููุฉ ู ู Integration Test
ุฅูุฑุฃ Vitestโ
import { type inferProcedureInput } from "@trpc/server";
import { expect, test } from "vitest";
import { appRouter, type AppRouter } from "~/server/router/_app";
import { createContextInner } from "~/server/router/context";
test("example router", async () => {
const ctx = await createContextInner({ session: null });
const caller = appRouter.createCaller(ctx);
type Input = inferProcedureInput<AppRouter["example"]["hello"]>;
const input: Input = {
text: "test",
};
const example = await caller.example.hello(input);
expect(example).toMatchObject({ greeting: "Hello test" });
});
Useful Resources
Resource | Link |
---|---|
tRPC Docs | https://www.trpc.ioโ |
Bunch of tRPC Examples | https://github.com/trpc/trpc/tree/next/examplesโ |
React Query Docs | https://tanstack.com/query/v4/docs/adapters/react-queryโ |