iTranslated by AI
A Lightweight Way to Use tRPC with Next.js Without withTRPC
Next.js APIs are convenient and easy to use, but they can be prone to issues when it comes to typing.
Using tRPC allows you to build very robust APIs.
A brief overview of using tRPC with Next.js
To briefly summarize the combination of tRPC and Next.js:
- As the name suggests, it is a library for performing RPC using TypeScript.
- By fixing a single endpoint and using it on the client, you can share types.
- It is robustly supported, even documented as Usage with Next.js.
- Since it differs from standard Next.js API routing, you use a single endpoint like
/api/trpc/[trpc]. - It has features like Context, which is great for managing common logic across multiple APIs in one place.
Wanting a lighter approach
With the combination with Next.js provided in the official documentation, I personally felt the following points were a bit of a downside:
- SSR is supported by using
withTRPCas an HOC for components, but it feels slightly excessive compared to the features I personally want.- It feels a bit excessive for cases like handling client-side authentication.
- Since I'm a fan of SWR, the part about using react-query is a bit problematic.
- Even if I use react-query, it's set to the older
v3series, so I'm not very enthusiastic about it. - By the way, there is also
trpc-swr, so if you want to use SWR while supporting SSR, this seems like a good choice.
- Even if I use react-query, it's set to the older
- There are similar issues if you have requirements to create multiple endpoints with different processing for
middlewareor Context.
Solution: Use the Vanilla Client instead of withTRPC
tRPC is a framework-agnostic library that is also available for plain TypeScript, so using it for the client side seemed to offer low dependency and high flexibility.
This time, I want to consider using the aforementioned Vanilla client to customize the following:
- Creating multiple endpoints
- Not using
withTRPC - Combining with SWR
Server-side
In this case, I want to consider a scenario where multiple endpoints are intentionally separated, so let's look at creating multiple endpoints as follows:[1]
/pages/api/user/trpc/[trpc].ts/pages/api/admin/trpc/[trpc].ts
The content can just be standard for Next.js. It's almost exactly like the documentation.
import * as trpc from '@trpc/server'
import * as trpcNext from '@trpc/server/adapters/next'
import { z } from 'zod'
export const adminRouter = trpc
.router()
.query('hello', {
input: z.object({ text: z.string().nullish() }).nullish(),
resolve({ input }) {
return {
greeting: `hello ${input?.text ?? 'world'}`,
}
},
})
// Change the router name
export type AdminAppRouter = typeof adminRouter
// export API handler
export default trpcNext.createNextApiHandler({
router: adminRouter,
createContext: () => null,
})
/pages/api/user/trpc/[trpc].ts is almost identical, so it is omitted here.
Client-side
Since createTRPCClient can be used straightforwardly, prepare clients with their respective endpoints by switching the url.
I'm using useMemo here, but depending on the use case, it might not necessarily be required to turn it into a hook.
// It's better to use `import type` as there were cases where importing Router types into the client caused build issues.
import type { AdminAppRouter } from "../app/server/AppRouter"
const useAdminTrpc = () => {
const client = useMemo(() => {
return createTRPCClient<AdminAppRouter>({
url: '/api/admin/trpc',
})
}, [])
return client
}
const useUserTrpc = () => {
const client = useMemo(() => {
return createTRPCClient<UserAppRouter>({
url: '/api/user/trpc',
})
}, [])
return client
}
On the component side, use it as follows:
const Greeting = () => {
const trpc = useAdminTrpc()
const { data } = useSWR("hello", () => {
return trpc.query("hello")
})
return <Box>
<Box>{data?.greeting}</Box>
</Box>
}
When combining with SWR, there is a possibility that keys might overlap more than when handling them via URLs, so some caution is needed here.
-
Normally, you can just use the merging feature, but there were cases where I wanted to separate them when combining middleware. ↩︎
Discussion