Providers
In ExpressoTS, providers act as modular components that encapsulate specific functionalities, such as email services, authentication, or database connections. This approach supports a loosely coupled architecture, allowing you to update or swap out these functionalities without impacting the rest of the system.
Key advantages
- Promote loose coupling between application layers.
- Simplify the testing process by decoupling logic from specific implementations.
- Enhance code maintainability and flexibility, supporting easy swaps of underlying services.
ExpressoTS harnesses providers to augment application capabilities, facilitating functionalities like email dispatching, data storage, and more, without tying the application logic to specific implementations.
Create a provider
Consider an application needing to send emails under various circumstances, such as user registration or password recovery. By implementing an email provider, ExpressoTS can send emails without directly interacting with the mailing service in the business logic.
In this example we will create a MailSenderProvider
to send emails using the nodemailer
library.
Let's use the CLI to scaffold a new provider named MailSenderProvider
:
expressots g p mailSender
Setting up the provider
The CLI will add the suffix Provider
to the provider name, creating a new file in the providers
directory. This file will contain the provider class, which you can then customize to suit your application's needs.
Here is the default provider file generated by the CLI:
import { provide } from "@expressots/core";
@provide(MailSenderProvider)
class MailSenderProvider {}
Implementing the provider
import nodemailer from "nodemailer";
import Mail from "nodemailer/lib/mailer";
export const enum EmailType {
Welcome = 0,
CreateUser,
ChangePassword,
Login,
RecoveryPassword,
}
@provide(MailSenderProvider)
export class MailSenderProvider {
private transporter: Mail;
constructor() {
this.transporter = nodemailer.createTransport({
host: Env.Mailtrap.HOST,
port: Env.Mailtrap.PORT,
auth: {
user: Env.Mailtrap.USERNAME,
pass: Env.Mailtrap.PASSWORD,
},
});
}
private mailSender(message: IMessage): Promise<void> {
await this.transporter.sendMail({
to: {
name: message.to.name,
address: message.to.email,
},
from: {
name: message.from.name,
address: message.from.email,
},
subject: message.subject,
html: message.body,
});
}
sendEmail(emailType: EmailType): Promise<void> {
switch (emailType) {
case EmailType.Login:
break;
case EmailType.Welcome:
break;
case EmailType.RecoveryPassword:
break;
case EmailType.ChangePassword:
break;
case EmailType.CreateUser:
this.MailSender({
to: {
name: "User",
email: Env.Mailtrap.INBOX_ALIAS,
},
from: {
name: "ExpressoTS",
email: "[email protected]",
},
subject: "Successfully logged in!",
body: "<h1>Welcome to the system!</h1>",
});
break;
}
}
}
This MailSenderProvider
abstracts the complexity of configuring and using nodemailer for email operations,
providing a straightforward method sendEmail to send different types of emails.
Using the provider
Here is the use case implementation making use of the MailSenderProvider
provider:
@provide(LoginUserUseCase)
export class LoginUserUseCase {
constructor(@inject(MailSenderProvider) private mailSender: MailSenderProvider) {}
execute(payload: ILoginUserRequestDTO): boolean {
const { email, password } = payload;
if (isAuthenticated(email, password)) {
return true;
}
mailSender.sendEmail(EmailType.Login);
return false;
}
}
In this use case, the MailSenderProvider
is injected through the constructor, leveraging ExpressoTS's dependency injection.
This decouples the email sending process from the authentication logic, illustrating the provider's role in maintaining clean and maintainable code.
Decorators for provider
ExpressoTS facilitates the registration of providers in its dependency injection system through the use of fluent decorators:
- @provide (default scope is request)
- @provideSingleton (singleton scope)
- @provideTransient (transient scope)
These decorators ensure that providers are automatically registered and resolved within the application's dependency injection system.
External providers
ExpressoTS promotes extensibility through the use of external providers. Developers can create reusable packages using the plugin design pattern to add new functionalities to an ExpressoTS application. This approach is ideal for sharing features across multiple projects or integrating third-party services.
The goal is to keep the core framework lean while enabling developers to extend their applications' capabilities with external providers.
Plugin pattern
The diagram illustrates the ExpressoTS plugin design pattern, demonstrating how external providers are seamlessly integrated into the client application through the Provider Manager.
It showcases the process of registering new providers and their respective lifecycle scopes, which can be singleton, request, or scoped.
Create external provider
The CLI can be used to scaffold a new external provider ( plugin ). Here's the command:
expressots create provider-name
This command generates a new directory with the provider's name and all the necessary files to start developing an external provider.
The generated package.json
file includes scripts for building, testing, and publishing the package.
An external provider should be a CommonJS package that meets the following requirements:
- Implement the
IProvider
interface in your provider class.
export interface IProvider {
name: string;
version: string;
author: string;
repo: string;
}
- Annotate your provider class with the
@injectable()
decorator for dependency injection. - Export your provider class in the
index.ts
file. - Publish your package to the npm registry.
import { IProvider } from "expressots";
@injectable()
class Logger implements IProvider {
log(message: string): void {
console.log(message);
}
}
Once the package is developed and published to the npm registry, it can be easily integrated into any ExpressoTS application.
Using external providers
Providers are registered in the App
class of an ExpressoTS application using the ProviderManager
.
this.Provider.register(Logger);
By default provider registration binds the provider as request-scoped. To change the scope, use the BindingScopeEnum
or string representation of the scope.
Example of registering a provider as a singleton:
this.Provider.register(Logger, "Singleton");
Adding external providers to an ExpressoTS application is a straightforward process that enhances the application's capabilities without bloating the core framework.
External provider best practices
When developing external providers for ExpressoTS, consider the following best practices:
- Interface Implementation: Ensure that external providers conform to the IProvider interface for consistency.
- Lifecycle Management: Choose the appropriate scope for the provider based on its intended usage pattern.
- Testing: Thoroughly test providers in isolation before integrating them into the main application.
- Documentation: Provide detailed usage instructions and configuration options for the external provider.
- Versioning: Follow semantic versioning practices to manage changes and updates to the provider.
- Error Handling: Implement robust error handling and logging mechanisms to ensure the provider's reliability.
- Security: Follow security best practices to protect sensitive data and prevent vulnerabilities in the provider, especially in the provider dependencies.
By adhering to these practices, developers can ensure that their external providers are robust, maintainable, and easily integrated into any ExpressoTS application.
Support us ❤️
ExpressoTS is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to your support. If you'd like to help, please read our support guide.