Skip to main content

Dependency Injection

Say your PetRoute needs to send a confirmation email whenever someone adopts a pet. It needs access to an EmailService. The obvious approach is to create one yourself, right there in the route:

class PetRoute {
private emailService = new EmailService();
}

This works, right up until EmailService itself needs something, like an API key from your configuration, or a logger. Now PetRoute needs to know how to construct all of that, too. Every class ends up needing to know how to build every other class it depends on, and if you ever want to swap in a different EmailService (a fake one for tests, say), you have to hunt down every new EmailService() in your codebase and change it.

Dependency injection flips this around: instead of a class building the things it needs, it simply asks for them, and something else is responsible for handing them over already built.

class PetRoute {
@Inject(EmailService)
private emailService?: EmailService;
}

PetRoute no longer knows or cares how an EmailService gets constructed. It just declares "I need one of these," and by the time PetRoute is actually used, emailService is already filled in.

Who's doing the handing over

The "something else" is a part of RapidREST called the ObjectFactory. Here's the sequence of events, end to end:

  1. Auto-Discovery finds every class you've exported under src/ and hands them to the ObjectFactory.
  2. When one of those classes is actually needed (a route handling an incoming request, say), the ObjectFactory creates an instance of it.
  3. While creating it, the ObjectFactory looks for any property marked with an injection decorator like @Inject, and fills each one in, creating those classes first if it needs to.
  4. Once every dependency is filled in, if the class has a method marked @Init, that method is called, so the class can do any setup that needs its dependencies to already be present.

You almost never call new SomeClass() yourself in RapidREST. You declare what you need with a decorator, and trust the ObjectFactory to have already built it for you by the time your code runs.

The injection decorators

@rapidrest/core gives you three of these:

  • @Inject — "give me an instance of this class." Works for any class the framework has discovered.
  • @Config — "give me this value from my configuration," instead of a class instance. See Configuration for what that config actually looks like.
  • @Logger — "give me a logger," pre-set up to tag every message with the name of the class that's logging.
class PetRoute {
@Inject(EmailService)
private emailService?: EmailService;

@Config('auth:secret')
private secret?: string;

@Logger
private logger?: LoggerInstance;
}

@rapidrest/service-core adds a further set of these specifically for reaching a database connection or a data repository, covered in Datastores & Connections and Repository & RepoUtils.

One instance, shared

By default, everyone who asks for @Inject(EmailService) gets the exact same EmailService instance, not a fresh one each time. This is called a singleton, and it's usually what you want: one shared connection, one shared cache, one shared copy of anything that doesn't need to be duplicated per-request. If you genuinely need more than one independently tracked instance of the same class, @Inject accepts a name option to ask for a specifically named one instead of the shared default.

Cleaning up

Mark a method @Destroy, and it's called automatically if that instance is ever removed, the counterpart to @Init for anything holding a resource (a connection, a file handle) that needs an explicit close.

Why bother with all of this?

  • No class constructs its own dependencies. A route, a service, a job, none of them know or care how the things they depend on get built.
  • Swapping implementations is trivial. Register a fake EmailService for tests, and every class using @Inject(EmailService) gets the fake automatically, with zero changes to those classes.
  • It composes with everything else. A plain class with no other framework decorators at all can still use @Inject, @Config, and @Logger, since Auto-Discovery already knows about it. There's no separate "make this injectable" step.