Inside create t3 app with Drizzle, why do we pass the database through ctx on each procedure?

If I'm not mistaken, we ideally don't want to do this if we want to break our application into separate layers (One of them being a data acess layer that we use to connect to our database). Would there be an issue if I remove this from the context and just import the db directly from the src/server/db/index.ts file? Default way of doing this in T3:
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
// simulate a slow db call
await new Promise((resolve) => setTimeout(resolve, 1000));

await ctx.db.insert(posts).values({ // NOTICE WE GRAB THE DB FROM CONTEXT
name: input.name,
});
}),
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
// simulate a slow db call
await new Promise((resolve) => setTimeout(resolve, 1000));

await ctx.db.insert(posts).values({ // NOTICE WE GRAB THE DB FROM CONTEXT
name: input.name,
});
}),
What I would rather do:
import { db } from "@/server/db";
...
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ input }) => {
// simulate a slow db call
await new Promise((resolve) => setTimeout(resolve, 1000));

await db.insert(posts).values({ // NOTICE WE GRAB THE DB FROM CONTEXT
name: input.name,
});
}),
import { db } from "@/server/db";
...
create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ input }) => {
// simulate a slow db call
await new Promise((resolve) => setTimeout(resolve, 1000));

await db.insert(posts).values({ // NOTICE WE GRAB THE DB FROM CONTEXT
name: input.name,
});
}),
Here's how it's being added to context now, I'd rather not use this? I feel like it's there for a reason though and I can't figure out why 😦
export const createTRPCContext = async (opts: { headers: Headers }) => {
return {
db,
...opts,
};
};
export const createTRPCContext = async (opts: { headers: Headers }) => {
return {
db,
...opts,
};
};
4 Replies
GBianchi
GBianchi4mo ago
Id like to know this too! I believe it is related to being able to use the correct db instance for each procedure. In prisma for example it's possible to create multiple extensions where each prisma client has a specific functionality. Maybe this is also true for drizzle. It's a good question though. Don't know the answer
Neto
Neto4mo ago
its just basic dependency injection if you want to import directly from drizzle, be
// src/lib/db.ts
export const db = drizzle(...)
// src/lib/db.ts
export const db = drizzle(...)
same effect, just one more import on the trpc file
DYELbrah
DYELbrah4mo ago
Sweet, I think I will use imports then. Specifically since this will allow our layered architecture to stay clean. We go from Router Layer > Service Layer > Data Access. At any time we can swap out Drizzle to any ORM in the Data Access layer which is great. Having an orm implementation directly in the ctx of TRPC forces us to commit to Drizzle / sort of hops from Data Access to Router.
cje
cje4mo ago
i would disagree that importing the db instead of semi-DIing it creates clearer separation of layers, you're still making all the same assumptions and have now made testing harder but yea importing works