t3-oss / create-t3-turbo

Clean and simple starter repo using the T3 Stack along with Expo React Native

Home Page:https://turbo.t3.gg

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

feat: A more robust trpc router structure and file separation organization like that of Cal.com

dBianchii opened this issue · comments

Describe the feature you'd like to request

As of a few months ago, @acme/validators was introduced to this repo. This was a great change, as we now are able to for example call useForm with the same exact same zod validator as the target api for that form's handleSubmit. We can call the validation logic even before leaving our network card which is awesome.

Alongside this change, I would highly recommend using Cal.com's api structure, which separates the router definitions from their respective handlers. There is a main reason for this: being able to call any handler from within any other handler.
We've seen how the question of "how do I call a procedure from within another procedure" causes some confusion to people: https://trpc.io/docs/server/server-side-calls (first image in this trpc page explains how to do it)

I have copied Cal.com's file structure to my repo, which you can check here and am using separate handlers. This vastly simplifies the ability of calling any dedicated handler from within anywhere else.

Describe the solution you'd like to see

Separated handlers from their definitions at the router, with _router.ts files at the root of the router folder, and multiple other handler files next to it.

Additional information

So, if this is something that we should do, I'd love to file a PR. For the purposes of this repo it's very simple since it's just 2 routers

Cal's reasoning was their lambdas got reaaally big so they needed to reduce size in order to reduce cold start latencies by lazy-loading every handler. I'd say they're one of the biggest trpc routers out there and for a normal size you won't see any benefits to this.

What's your reasoning for this?

Cal's reasoning was their lambdas got reaaally big so they needed to reduce size in order to reduce cold start latencies by lazy-loading every handler. I'd say they're one of the biggest trpc routers out there and for a normal size you won't see any benefits to this.

What's your reasoning for this?

Not referencing their UNSTABLE_CACHE stuff. Just mainly the way they separate routers from handlers. You can also see it in my repo I mentioned, which is what I believe most trpc users should follow imo. It's mainly for making it easier to call any handler from any other one.

The router declarations would look like this:

export const userRouter = {
  changeName: protectedProcedure
    .input(ZChangeNameInputSchema)
    .mutation(changeNameHandler),
  getAll: publicProcedure.query(getAllHandler),
  getNotifications: protectedProcedure.query(getNotificationsHandler),
  getOne: protectedProcedure.input(ZGetOneInputSchema).query(getOneHandler),
  switchActiveTeam: protectedProcedure
    .input(ZSwitchActiveTeamInputSchema)
    .mutation(switchActiveTeamHandler),
} satisfies TRPCRouterRecord;

Notice how the handlers are defined elsewhere, allowing for them to be called very naturally from any other endpoint

I personally don't think this is a good default. If this works better for you then go for it! But you're separating things before you even know you need that separation, resulting in a lot of jumping back and forth between packages (assuming all your validators are put in @acme/validators.

For someone new coming to a repo, I think it's easier to reason about "functions", which is why I advocate for shared code to be broken out into their own functions:

// api/src/some-file.ts
export function getThing(id: string) { ... }

// api/src/router-1.ts
  someProc: protectedProcedure.input(...).query(() => {
    const thing = getThing(input.id)
  }),

// api/src/router-2.ts
  someOtherProc: protectedProcedure.input(...).query(() => {
    const thing = getThing(input.id)
  }), 

In this (contrived) example, why should i forward the entire procedure's input and context when it only needs an id?

I just struggle to see a proper use-case when a handler should call another handler.

We can keep the issue open if anyone else wanna chime in, but for me it's not an improvement and raises the entrybar for newcomers