Captain
Captainβ€’2y ago

How to do dependecy injection?

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?
30 Replies
Captain
Captainβ€’2y ago
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
Nick
Nickβ€’2y ago
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 πŸ™‚
Captain
Captainβ€’2y ago
thats true. but that would mean i need to create a middleware for eacht routegroup
Nick
Nickβ€’2y ago
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
Alex / KATT 🐱
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!
Captain
Captainβ€’2y ago
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.
Captain
Captainβ€’2y ago
Captain
Captainβ€’2y ago
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
Alex / KATT 🐱
thank you @Captain! keep us in the loop of how you solve this, DI is always an interesting problem
Unknown User
Unknown Userβ€’2y ago
Message Not Public
Sign In & Join Server To View
Captain
Captainβ€’2y ago
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
Unknown User
Unknown Userβ€’2y ago
Message Not Public
Sign In & Join Server To View
Captain
Captainβ€’2y ago
ahso. thats what im doing too. i moved all util to the nextjs app and the rest as a node lib in NX workspace
Nick
Nickβ€’2y ago
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
Trpc Standalone Server Example - StackBlitz
Run official live example code for Trpc Standalone Server, created by Trpc on StackBlitz
Nick
Nickβ€’2y ago
Usage
Nick
Nickβ€’2y ago
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.
Captain
Captainβ€’2y ago
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
isaac_way
isaac_wayβ€’2y ago
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.
Alex / KATT 🐱
How do you share the schema with the front-end?
isaac_way
isaac_wayβ€’2y ago
Anything wrong with having a separate package with the schemas and importing from there?
Alex / KATT 🐱
Nope, lgtm
Alex / KATT 🐱
i'd like to get your POV on this: https://github.com/trpc/trpc/pull/3727
GitHub
play with router classes by KATT Β· Pull Request #3727 Β· trpc/trpc
Preface I wouldn't say I like OO, but these classes are neat ways of encapsulating logic & makes it more natural to make private methods and calling stuff between classes etc . What it does...
isaac_way
isaac_wayβ€’2y ago
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?
Alex / KATT 🐱
yeah, would need to do toRouter(myService).createCaller()
isaac_way
isaac_wayβ€’2y ago
cool yeah I don't think that's a problem just curious
Alex / KATT 🐱
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
isaac_way
isaac_wayβ€’2y ago
i sent the code example to them already they liked it πŸ˜„ and yeah i definitely see this making things easier for our backend guys
Alex / KATT 🐱
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
isaac_way
isaac_wayβ€’2y ago
@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();
Alex / KATT 🐱
clone it and have a play!