Step-by-Step Guide to Integrate ModuleRegistry into Your Application
Integrating the ModuleRegistry class into an existing application can greatly enhance the modularity and scalability of your dependency injection setup. This guide will walk you through the steps to integrate ModuleRegistry with a given starting example.
Starting Example
Let's begin with a sample application that utilizes @wroud/di for dependency injection. This example demonstrates how to set up a service container and register various services:
import { ServiceContainerBuilder, injectable, createService } from "@wroud/di";
@injectable(() => [])
class DatabaseConnection {}
@injectable(() => [DatabaseConnection])
class Database {}
@injectable(() => [Database])
class DBUsers {}
@injectable(() => [Database])
class DBArticles {}
@injectable(() => [Database])
class DBComments {}
@injectable(() => [])
class Request {}
@injectable(() => [Request, DBUsers])
class Profile {}
@injectable(() => [Database])
class SessionStore {}
@injectable(() => [Request, SessionStore])
class Session {}
@injectable(() => [DatabaseConnection, GQLServer])
class App {}
@injectable(() => [Request])
class GQLServer {}
const serviceCollection = new ServiceContainerBuilder()
.addSingleton(App)
.addSingleton(DatabaseConnection)
.addSingleton(GQLServer)
.addTransient(Database)
.addTransient(SessionStore)
.addTransient(DBUsers)
.addTransient(DBArticles)
.addTransient(DBComments)
.addScoped(Request)
.addScoped(Profile)
.addScoped(Session);In this example, we have a set of services such as DatabaseConnection, Database, and App, among others. These services are registered in the ServiceContainerBuilder with different lifetimes (singleton, transient, and scoped).
Grouping Dependencies into Modules
To manage these dependencies more efficiently, we can group related services into modules. This approach helps in organizing the code better and simplifies the registration process. Each module will represent a cohesive set of related services.
Why Grouping Dependencies is Beneficial
Code Organization: Grouping related services into modules helps in maintaining a clean and organized codebase. It becomes easier to locate, manage, and update services related to a specific functionality.
Scalability: As your application grows, the number of services and their dependencies can become overwhelming. Grouping services into modules allows you to scale your application more effectively by isolating changes and updates to specific parts of the application.
Reusability: Modules can be reused across different parts of the application or even in different projects. This promotes code reuse and reduces duplication.
Maintainability: By grouping services into logical modules, maintaining and updating the code becomes more manageable. Changes to a specific functionality are confined to the respective module, reducing the risk of unintended side effects.
Method for Grouping Dependencies
We can achieve this by defining modules as collections of related services. Each module will have a unique name and a method to configure the services related to that module. Here are some suggested groupings for the given services:
Core Module: This module can contain core services that are fundamental to the application, such as the main application class, database connection, and any server configurations.
Database Module: This module can group all services related to database interactions, such as the database itself and entities that interact with the database.
Session Module: This module can include services related to user sessions and requests, such as session storage and profile management.
By following this modular approach, you can ensure that your services are well-organized, easily maintainable, and scalable.
Creating a Module
To create a module, follow these steps:
Group Related Services: Organize related services by creating a new package for each module if you are using workspaces. This helps in keeping the services that belong to the same module together, making the code more organized and manageable.
Create
module.ts: In the package where you have grouped the related services, create a file namedmodule.ts. In this file, useModuleRegistry.addto register the module. This will allow the module and its services to be recognized and managed by theModuleRegistry.Import
module.tsinindex.ts: In the same package, create anindex.tsfile and importmodule.tsto ensure the module is registered when the package is imported.Add
module.tsandindex.tsto "sideEffects": To ensure that the module is correctly registered when the package is imported, addmodule.tsandindex.tsto thesideEffectsfield in yourpackage.json. This step ensures that the module registration side effect is executed, allowing the module to be properly integrated into the application.
Example: Core Module
Group Related Services: Create a package named
@my/coreand add the core services.Create
module.ts: In the@my/corepackage, create amodule.tsfile:
import { ModuleRegistry } from "@wroud/di";
import { App } from "./App";
import { GQLServer } from "./GQLServer";
ModuleRegistry.add({
name: "@my/core",
async configure(serviceCollection) {
serviceCollection.addSingleton(App).addSingleton(GQLServer);
},
});- Create
index.ts: In the@my/corepackage, create anindex.tsfile and importmodule.ts:
import "./module.ts";- Add
module.tsandindex.tsto "sideEffects": In yourpackage.jsonof the@my/corepackage, add the paths tomodule.tsandindex.ts:
{
"name": "@my/core",
"version": "1.0.0",
"main": "index.js",
"sideEffects": ["./src/module.ts", "./src/index.ts"]
}Example: Database Module
Group Related Services: Create a package named
@my/databaseand add the database-related services.Create
module.ts: In the@my/databasepackage, create amodule.tsfile:
import { ModuleRegistry } from "@wroud/di";
import { Database } from "./Database";
import { DBUsers } from "./DBUsers";
import { DBArticles } from "./DBArticles";
import { DBComments } from "./DBComments";
import { DatabaseConnection } from "./DatabaseConnection";
ModuleRegistry.add({
name: "@my/database",
async configure(serviceCollection) {
serviceCollection
.addTransient(Database)
.addTransient(DBUsers)
.addTransient(DBArticles)
.addTransient(DBComments)
.addSingleton(DatabaseConnection);
},
});- Create
index.ts: In the@my/databasepackage, create anindex.tsfile and importmodule.ts:
import "./module.ts";- Add
module.tsandindex.tsto "sideEffects": In yourpackage.jsonof the@my/databasepackage, add the paths tomodule.tsandindex.ts:
{
"name": "@my/database",
"version": "1.0.0",
"main": "index.js",
"sideEffects": ["./src/module.ts", "./src/index.ts"]
}Example: Session Module
Group Related Services: Create a package named
@my/sessionand add the session-related services.Create
module.ts: In the@my/sessionpackage, create amodule.tsfile:
import { ModuleRegistry } from "@wroud/di";
import { Request } from "./Request";
import { Session } from "./Session";
import { SessionStore } from "./SessionStore";
import { Profile } from "./Profile";
ModuleRegistry.add({
name: "@my/session",
async configure(serviceCollection) {
serviceCollection
.addTransient(SessionStore)
.addScoped(Request)
.addScoped(Profile)
.addScoped(Session);
},
});- Create
index.ts: In the@my/sessionpackage, create anindex.tsfile and importmodule.ts:
import "./module.ts";- Add
module.tsandindex.tsto "sideEffects": In yourpackage.jsonof the@my/sessionpackage, add the paths tomodule.tsandindex.ts:
{
"name": "@my/session",
"version": "1.0.0",
"main": "index.js",
"sideEffects": ["./src/module.ts", "./src/index.ts"]
}By following these steps for each module, you can create and register modules in a structured manner, making your application more modular and easier to manage. In the next section, we will discuss how to use the ModuleRegistry to configure the service container.
Initializing the Service Container
Now that we have organized our services into modules and registered them with ModuleRegistry, the next step is to create an entry point for our application and use ServiceContainerBuilder with ModuleRegistry to initialize the service collection.
Creating the Entry Point
Create an Entry Point File: Create a new file named
main.tsorindex.tsin the root of your application. This file will serve as the entry point for your application.Initialize ServiceContainerBuilder: In the entry point file, use
ServiceContainerBuilderto initialize the service collection. Iterate over the modules registered inModuleRegistryand configure the service collection.
Example: Entry Point
Here is an example of how to create the entry point and initialize the service collection:
import { ServiceContainerBuilder, ModuleRegistry } from "@wroud/di";
import { App } from "@my/core";
// Create a new ServiceContainerBuilder instance
const builder = new ServiceContainerBuilder();
// Iterate over the registered modules in ModuleRegistry
for (const module of ModuleRegistry) {
await module.configure(builder);
}
// Build the service container
const serviceProvider = builder.build();
// Now you can resolve and use your services
const app = serviceProvider.getService(App);
app.start();In this example:
- We create a new instance of
ServiceContainerBuilder. - We iterate over the modules registered in
ModuleRegistryand call theirconfiguremethod to register their services with the service collection. - We build the service container using the
buildmethod ofServiceContainerBuilder. - We resolve the
Appservice from the service provider and start the application.
By following these steps, you can ensure that all your services are properly registered and configured, and your application is ready to run.
How It Works
In this setup, modules are registered automatically due to the way we have structured our imports and module initialization:
Automatic Module Registration: Each module's
module.tsis imported in the package's main fileindex.ts. This means that whenever anything is imported from these packages, their respective modules are registered automatically.For example:
// @my/core/index.ts
import "./module.ts";
// @my/database/index.ts
import "./module.ts";
// @my/session/index.ts
import "./module.ts";Dependency Chain: In our example, the
Appclass has a dependency onDatabaseConnectionwhich causes the@my/databasemodule to be registered. Additionally,Appalso depends onGQLServer, which in turn depends onRequest, causing the@my/sessionmodule to be registered.- Core Module: We imported
Appin our entry point, which registered@my/core. - Database Module:
Appdepends onDatabaseConnection, which triggers the registration of@my/database. - Session Module:
Appalso has a dependency onGQLServer, which depends onRequest, triggering the registration of@my/session.
- Core Module: We imported
By importing the main files of each package, we ensure that all necessary modules are registered without explicitly calling their registration code in the entry point. This method simplifies the initialization process and ensures that all dependencies are properly configured.
By following these steps, you can ensure that all your services are properly registered and configured, and your application is ready to run.
Conclusion
In this guide, we have demonstrated how to integrate the ModuleRegistry from @wroud/di into an existing application to manage and scale your dependencies effectively. By organizing your services into cohesive modules and using the ModuleRegistry to handle their registration, you can achieve a more modular, scalable, and maintainable application architecture.
Key Takeaways
- Modular Organization: Group related services into modules to maintain a clean and organized codebase.
- Automatic Module Registration: Use
module.tsand import it in the package's main file (index.ts) to ensure modules are registered automatically when the package is imported. - ServiceContainerBuilder Integration: Initialize the service container by iterating over the registered modules and configuring the service collection using
ServiceContainerBuilder. - Dependency Chain Management: Leverage the dependency chain to automatically register necessary modules based on the services' dependencies.
By following these best practices, you can streamline your application's dependency injection setup, making it easier to manage and scale as your application grows. The ModuleRegistry provides a robust solution for handling the complexities of dependency management in large-scale applications, ensuring that your services are properly registered and accessible throughout the application lifecycle.