iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐯

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 withTRPC as 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 v3 series, 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.
  • There are similar issues if you have requirements to create multiple endpoints with different processing for middleware or 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.

脚注
  1. Normally, you can just use the merging feature, but there were cases where I wanted to separate them when combining middleware. ↩︎

GitHubで編集を提案

Discussion