Skip to Content

Coming from .NET

The container you know, in TypeScript.

@sukujs/di follows Microsoft.Extensions.DependencyInjection closely. If you know IServiceCollection and IServiceProvider, you know most of it.

.NETSuku
IServiceCollectionServiceCollection
services.AddSingleton<IFoo, Foo>()services.addSingleton(Foo, FooImpl) or singleton(Foo, FooImpl)
AddScoped, AddTransientaddScoped, addTransient
TryAddSingletontryAddSingleton
AddKeyedSingleton<T>(key)addKeyedSingleton(T, key, Impl)
[FromKeyedServices(key)]@Inject(T, key)
services.Replace(...)services.replace(...)
BuildServiceProvider(validateScopes, validateOnBuild)build({ validateScopes, validateOnBuild })
GetRequiredService<T>()provider.get(T)
GetService<T>()provider.getOptional(T)
GetServices<T>()provider.getAll(T)
CreateScope()provider.createScope()
IDisposable, IAsyncDisposabledispose(), [Symbol.dispose], [Symbol.asyncDispose]
IOptions<T>defineConfig(name, schema)
ILoggerLogger
HttpContextRequestContext
Middleware (context, next)Middleware (ctx, next)
Minimal API app.MapGet("/users/{id}", ...)route.get('/users/:id', { ... })
Controller classA @Routes class
Model binding and validationThe route's params, query and body schemas
Action filtersMiddlewares
Exception filters, UseExceptionHandleronError

Interfaces become abstract classes

TypeScript interfaces are erased at compile time, so they cannot be tokens. Use an abstract class in the same role.

export abstract class IClock {
  abstract now(): Date;
}

services.addSingleton(IClock, SystemClock);

class Invoices {
  constructor(private readonly clock: IClock) {}
}

Same rules, same errors

  • The last registration for a token wins; getAll returns them all in order.
  • Resolving a scoped service from the root provider throws.
  • A singleton that depends on a scoped service is rejected as a captive dependency.
  • Disposal runs in reverse order of creation. Instances you registered yourself are left alone.

Routes classes are singletons by default

In ASP.NET Core a controller is created per request. A Suku @Routes class is a singleton unless you ask otherwise, which avoids an allocation per request. Request-bound services are usually taken per route instead.

place = route.post('/', {
  body: PlaceOrder,
  services: { uow: UnitOfWork },     // scoped: one per request
  handle: ({ body, uow }) => uow.orders.place(body),
});

The composition root

Suku.create plays the part of Program.cs.

const app = await Suku.create({
  features: [OrdersFeature],
  services: [singleton(IClock, SystemClock)],
  configureServices: (services) => services.addScoped(UnitOfWork),
  adapter: nodeAdapter(),
});

app.use(requestLogging);
await app.listen(5000);

On this page