T
tRPC

How to do dependecy injection?

How to do dependecy injection?

CCaptain1/11/2023
My routes are grouped by using the router object. i'd like to be able to inject a service to each route group. is this possible and how can i do this? im thinking of something like this. but typescript does not seem to like this
type RouterWithInjectedServices<T> = (service: T) => AnyRouter

export const greeting: RouterWithInjectedServices<Greeting> = (service) => router({
hello: procedure.query(() => {

return service.hello()
}),

goodbye: procedure.query(() => {
return service.goodbye()
})

})
type RouterWithInjectedServices<T> = (service: T) => AnyRouter

export const greeting: RouterWithInjectedServices<Greeting> = (service) => router({
hello: procedure.query(() => {

return service.hello()
}),

goodbye: procedure.query(() => {
return service.goodbye()
})

})
im looking for the right return type for the RouterWithInjectedServices type im getting the error when i add this object to the router.
Type 'RouterWithInjectedServices<Greeting>' is not assignable to type 'AnyProcedure | AnyRouter'.
Type 'RouterWithInjectedServices<Greeting>' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
Type 'RouterWithInjectedServices<Greeting>' is not assignable to type 'AnyProcedure | AnyRouter'.
Type 'RouterWithInjectedServices<Greeting>' is missing the following properties from type 'Procedure<"query" | "mutation" | "subscription", any>': _type, _def, _procedure
Nnlucas1/11/2023
tRPC has Context for this. You can also extend the Context using Middlewares. This is all well documented so you can have a read up on those pages πŸ™‚
CCaptain1/11/2023
thats true. but that would mean i need to create a middleware for eacht routegroup
Nnlucas1/11/2023
Depending on your DI approach you could also configure Context with a factory(s) function to get things from DI then each procedure can just request what it needs It’s fairly normal to have multiple base procedures though, so you can create a base procedure co-located above the router with the right middlewares composed together to inject your services. It’s no more boilerplatey than composing your service directly on the router
AKAlex / KATT 🐱1/11/2023
do a base procedure per router group that has the services you want or you can create each router group in a callback function
const appRouter = router({
post: createPostRouter({ postService })
})

function createPostRouter(services: { postService: PostService }) {
return router({ /* ... */ })
}
const appRouter = router({
post: createPostRouter({ postService })
})

function createPostRouter(services: { postService: PostService }) {
return router({ /* ... */ })
}
potentially the latter can be done with a DI library like tsyringe curious what you decide on being nicest, keep us in the loop!
CCaptain1/11/2023
That sounds exactly like what i am looking for. Thank you both so much! Im going to try tsyringe and see if i can get it to work. I'll definitely post here what i come up with. Thank you both so much for your help and the great library.
CCaptain1/11/2023
CCaptain1/11/2023
I have learned so much in the past 2 months by just trying to understand trpc and how its connected with each other. Again thank you so much for your support
AKAlex / KATT 🐱1/11/2023
thank you @Captain! keep us in the loop of how you solve this, DI is always an interesting problem
UUUnknown User1/12/2023
5 Messages Not Public
Sign In & Join Server To View
CCaptain1/12/2023
im not sure if this would work in my case. im using supabase with row level security and the supabase client is dependent on the req,res object. i have no base clase which export the supabase client. so DI is in the end not the best solution for this. for now im gonna go with service middleware for each route group im not sure what u mean with monorepo standalone package and binding the trp router, could you elaborate? i have a node lib in my case which exports the router
UUUnknown User1/12/2023
Message Not Public
Sign In & Join Server To View
CCaptain1/12/2023
ahso. thats what im doing too. i moved all util to the nextjs app and the rest as a node lib in NX workspace
Nnlucas1/14/2023
Been thinking about this one recently as I am convinced that the code above is needlessly complex. Wrapping procedures/routers in DI factories just seems not great to me. Have had a play with tsyringe and some trpc/typescript magic and have a rough prototype here which seems to work perfectly albeit could be tidied up usage-wise: https://stackblitz.com/edit/github-ewmssd?file=src/server.ts
Nnlucas1/14/2023
Usage
Nnlucas1/14/2023
Some usage details could be fairly easily refactored towards a nicer experience. I think something like
const baseProcedure = /* auth, logging, etc */
const globalInjector = createInjector(container, { foo: Foo, bar: Bar })

// opts.ctx.bar does not get added because it's not requested
const routeGroupProcedure = baseProcedure.use(globalInjector({ foo: true }))

const appRouter = t.router({
thing: routeGroupProcedure.query(async opts => {
return await opts.ctx.foo.doStuff()
}),
otherThing: routeGroupProcedure.query(async opts => {
return await opts.ctx.foo.doOtherStuff()
}),
})
const baseProcedure = /* auth, logging, etc */
const globalInjector = createInjector(container, { foo: Foo, bar: Bar })

// opts.ctx.bar does not get added because it's not requested
const routeGroupProcedure = baseProcedure.use(globalInjector({ foo: true }))

const appRouter = t.router({
thing: routeGroupProcedure.query(async opts => {
return await opts.ctx.foo.doStuff()
}),
otherThing: routeGroupProcedure.query(async opts => {
return await opts.ctx.foo.doOtherStuff()
}),
})
Nice thing about moving this inside a middleware is we could in theory account for niceties of DI, like transient/request/singleton scoping, while only instantiating what's needed for a given request.
CCaptain1/15/2023
looks very interesting nick i spent sometime with this too. in the end, i am using Tsyringe to do IoC. one thing i am unable to solve is the need to register a middleware to resolve the service that is needed right now, i have a base class who's params are resolved using useValue. all other classes are register with the container using useClass. i do still need to resolve it in a middleware and bind it to the context
Iisaac_way1/16/2023
This is a really interesting discussion - The way our backend devs have been doing it is entirely separate from tRPC which I find really interesting compared with the approaches in this discussion. It decouples completely from the trpc code (not saying it's better or worse, just very different from the above approaches). Basically we have these separate "service" classes that contain all DB interactions, "Controller" classes that contain the resolvers. Each of these may have private members which are services they're dependent on, which are passed when initializing the services (so you can pass in mocks and such). Allows testing of the controllers and services independently of tRPC. IE:
export class AuthController {
private authService: AuthService
private codeService: CodeService
private emailService: EmailService
private notificationUtil: NotificationUtilities
private smsService: SmsService
private userService: UserService
// ...
export class AuthController {
private authService: AuthService
private codeService: CodeService
private emailService: EmailService
private notificationUtil: NotificationUtilities
private smsService: SmsService
private userService: UserService
// ...
Then our router is just like:
.mutation('loginWithEmailAndPassword', {
input: LoginInputSchema,
output: TokensAndUserOutputSchema,
resolve: authController.login,
})
.mutation('loginWithEmailAndPassword', {
input: LoginInputSchema,
output: TokensAndUserOutputSchema,
resolve: authController.login,
})
AuthController contains the resolvers, and uses the private members to do stuff. This is v9 btw πŸ˜… Not sure if this solves your problem, but it's how we've done dependency injection and it seems really straightforward. I haven't tried doing injection via contexts so IDK how it compares.
AKAlex / KATT 🐱1/16/2023
How do you share the schema with the front-end?
Iisaac_way1/16/2023
Anything wrong with having a separate package with the schemas and importing from there?
AKAlex / KATT 🐱2/6/2023
Nope, lgtm i'd like to get your POV on this: https://github.com/trpc/trpc/pull/3727
Iisaac_way2/6/2023
this would be huge - would eliminate a lot of boilerplate we're currently writing and at first glance I don't see any drawback from our current approach. I guess the one change from our current approach is that the trpc methods wouldn't be directly callable, would need to go through createCaller yeah?
AKAlex / KATT 🐱2/6/2023
yeah, would need to do toRouter(myService).createCaller()
Iisaac_way2/6/2023
cool yeah I don't think that's a problem just curious
AKAlex / KATT 🐱2/6/2023
have some peeps on your team have a look at the PR, would love some feedback i'm going to do the same this week, we also use some "service classes" at work and i think it's really annoying to jump between files i hate context switching
Iisaac_way2/6/2023
i sent the code example to them already they liked it πŸ˜„ and yeah i definitely see this making things easier for our backend guys
AKAlex / KATT 🐱2/6/2023
if any of you wanna fork that PR and play around with any of the todos that would be cool too you do some sort of DI i think wanna make sure it works for people's common DI patterns
Iisaac_way2/6/2023
@alex / KATT hmm yeah we pass dependencies to our controllers via the class constructor. With this method we could do something like this?
const myRouter = toRouter(new Controller({someService: new SomeService()}))
const caller = myRouter.createCaller({ctx});
caller.myMethod();
const myRouter = toRouter(new Controller({someService: new SomeService()}))
const caller = myRouter.createCaller({ctx});
caller.myMethod();
AKAlex / KATT 🐱2/6/2023
clone it and have a play!

Looking for more? Join the community!

T
tRPC

How to do dependecy injection?

Join Server
Recommended Posts
A lot of WHYGreat post from @cje on Twitter but also got a lot of questions 1. If the data flow starts from `How are they defined separately?I am using monorepo for my project design and I want to separate the definition of trpc completely ihow can i get procedure name?is it possible to get procedure name? i would like to append a service to ctx based on procedure namtRPC sockets with reactTrying to make my React app work with socket with minimal server but getting error: "Uncaught TypeEtransformers, tensor.js, PyTorch and tRPCdumb question: does anyone has experience with tensorflow.js? is there any major obstacle to use tensubscriptionHow do you guys Authenticate / Authorize Subscription ?NextJS and createProxySSGHelpers context type errorHi guys, do you guys know a better way of clean this typescript error? createProxySSGHelpers({ Validating PermissionsHi! A common operation that I'm doing in tRPC is validating that a person has permissions to perforAny typescript chads able know if it's possible to map a type to another type w genericsNot 100% sure if this is appropriate to ask here but I figured there's a lot of good TS developers ouseQuery enabled not working???Even setting the `enabled` to false --> `trpc.order.get({id: "123"}, {enabled: false})` still make`QueryClientProvider` not included in `withTRPC`?Trying to use `import { useIsMutating } from "react-query"` but it's throwing `Error: No QueryClientHandle React error boundarySeems like Im doing something wrong as I can't handle the error boundary from trpc query. I've queryAny tips for Typescript slowness?We successfully migrated from 9.x to 10 but are still seeing slow VS Code perf (10+ seconds to get aChange status of useMutation from 'success' back to 'idle'Hiya, I have a mutation for creating stuff that can run a few different ways on a page, and I want tIs it possible to call one procedure in another?I have multiple routers in my trpc server. For example userRouter (e.g. procedure: getUser) and post