Decorator-based containers do not work well in React Server Components. Here is the small alternative we use instead.
Most TypeScript DI libraries lean on decorators and reflect-metadata. That is awkward in a React Server Components app: the metadata emit is a compile-time feature, and pulling a reflection polyfill into a server bundle is a cost with no return.
What you actually need from a container is narrower than what those libraries offer. You need a way to name a dependency, a way to say how it is built, and a way to ask for it. Everything else is convenience.
A typed token carries the interface at the type level. The token itself is a symbol, so two tokens with the same description never collide. Because the type parameter is phantom, resolving a token returns a precisely typed instance with no casts at the call site.
Lifetimes matter more than they look. A singleton is created once per container. A scoped instance is created once per child scope, which maps neatly onto a request. Transient means a new instance each time. Getting this wrong is how request state leaks between users, so it is worth making explicit rather than implicit.
The real payoff is the composition root. One file decides that the content repositories talk to Supabase in production and to in-memory fixtures in tests. Nothing else in the codebase knows which, and no page imports a database client. Swapping a backing store becomes a one-line edit in one file.