@devoven/email
v0.1.1
Published
Email Package for NestJS Hexagonal Architecture
Downloads
445
Readme
@devoven/email
Email delivery service for NestJS. Send raw and templated emails via SMTP or a console sink for local development.
Installation
npm install @devoven/email
# or
pnpm add @devoven/emailPeer Dependencies
Any NestJS application already includes the standard peers (@nestjs/common, @nestjs/core, rxjs, reflect-metadata). No extra install is needed for those.
If you want to send emails via SMTP, install the optional nodemailer peer:
npm install nodemailerQuick Start
import { EmailModule } from '@devoven/email';
@Module({
imports: [
EmailModule.register({
defaultSender: '[email protected]',
}),
],
})
export class AppModule {}Without smtpConfig the module uses ConsoleEmailProvider, which prints emails to stdout. Pass smtpConfig to switch to NodemailerProvider.
Module Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| defaultSender | string | — (required) | The from address used when no sender is specified on the message |
| smtpConfig | SmtpConfig | undefined | SMTP connection config. When provided, NodemailerProvider is used; otherwise ConsoleEmailProvider |
| smtpConfig.host | string | — | SMTP server hostname |
| smtpConfig.port | number | — | SMTP server port |
| smtpConfig.secure | boolean | — | Use TLS. When true, auth is required |
| smtpConfig.auth.user | string | — | SMTP username |
| smtpConfig.auth.pass | string | — | SMTP password |
| templateRegistry | Class or instance | InMemoryEmailRepository | Custom EmailTemplateRepositoryPort implementation |
| templateRenderer | Class or instance | SimpleEmailTemplateRenderer | Custom EmailTemplateRenderPort implementation |
| templates | EmailTemplateValueObject[] | [] | Templates seeded into the repository on module init |
| global | boolean | false | Register the module globally |
SMTP validation rules
- Both
hostandportmust be provided together. - When
secureistrue,authmust be provided. - When
authis provided, bothuserandpassare required.
Async Registration
Use registerAsync when SMTP credentials come from environment variables or a config service:
import { EmailModule } from '@devoven/email';
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
EmailModule.registerAsync({
useFactory: (config: ConfigService) => ({
defaultSender: config.get('MAIL_FROM'),
smtpConfig: {
host: config.get('SMTP_HOST'),
port: config.get<number>('SMTP_PORT'),
secure: config.get<boolean>('SMTP_SECURE'),
auth: {
user: config.get('SMTP_USER'),
pass: config.get('SMTP_PASS'),
},
},
}),
inject: [ConfigService],
global: true,
}),
],
})
export class AppModule {}Sending Emails
Inject TOKENS.SendEmailPort for raw messages or TOKENS.SendTemplateEmailPort for template-based emails. Both tokens are exported from the module.
Raw email
import { Inject, Injectable } from '@nestjs/common';
import { TOKENS, SendEmailPort, EmailMessageValueObject } from '@devoven/email';
@Injectable()
export class NotificationService {
constructor(
@Inject(TOKENS.SendEmailPort)
private readonly sendEmail: SendEmailPort,
) {}
async sendWelcome(to: string) {
const message = EmailMessageValueObject.create({
recipients: to,
subject: 'Welcome!',
html: '<p>Thanks for signing up.</p>',
});
await this.sendEmail.execute(message);
}
}Templated email
import { Inject, Injectable } from '@nestjs/common';
import { TOKENS, SendTemplateEmailPort } from '@devoven/email';
@Injectable()
export class NotificationService {
constructor(
@Inject(TOKENS.SendTemplateEmailPort)
private readonly sendTemplate: SendTemplateEmailPort,
) {}
async sendPasswordReset(to: string, resetUrl: string) {
await this.sendTemplate.execute('password-reset', {
recipients: to,
variables: { resetUrl },
});
}
}Registering templates at startup
Pass templates to seed the repository when the module initialises:
import { EmailModule, EmailTemplateValueObject } from '@devoven/email';
EmailModule.register({
defaultSender: '[email protected]',
templates: [
EmailTemplateValueObject.create({
name: 'password-reset',
subject: 'Reset your password',
html: '<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>',
}),
],
})The built-in SimpleEmailTemplateRenderer replaces ${variableName} placeholders with values from the variables map.
Architecture
The module follows hexagonal architecture. The two driving ports are exported and available for injection in consuming services.
Port / Token Mapping
| Token | Interface | Default Implementation | Purpose |
|-------|-----------|------------------------|---------|
| TOKENS.SendEmailPort | SendEmailPort | SendEmailUseCase | Send a raw EmailMessageValueObject |
| TOKENS.SendTemplateEmailPort | SendTemplateEmailPort | SendTemplateEmailUseCase | Look up a named template and send a rendered email |
| TOKENS.EmailProviderPort | EmailProviderPort | ConsoleEmailProvider or NodemailerProvider | Delivery transport |
| TOKENS.EmailTemplateRepositoryPort | EmailTemplateRepositoryPort | InMemoryEmailRepository | Store and retrieve email templates |
| TOKENS.EmailTemplateRenderPort | EmailTemplateRenderPort | SimpleEmailTemplateRenderer | Render template strings with variable substitution |
The two driving ports (SendEmailPort, SendTemplateEmailPort) and the template repository port (EmailTemplateRepositoryPort) are exported from the module so consuming services can inject them.
Driven port interfaces
EmailProviderPort — delivery transport:
interface EmailProviderPort {
send(emailMessage: EmailMessageValueObject): Promise<void>;
}EmailTemplateRepositoryPort — template storage:
interface EmailTemplateRepositoryPort {
findByName(name: string): Promise<EmailTemplateValueObject | undefined>;
register(template: EmailTemplateValueObject): Promise<void>;
}EmailTemplateRenderPort — template rendering:
interface EmailTemplateRenderPort {
render(template: string, variables: Record<string, unknown>): string;
}Domain Model
EmailMessageValueObject
Created via EmailMessageValueObject.create({ recipients, subject, html?, text?, from?, cc?, bcc? }). At least one of html or text is required. All email addresses are validated on construction.
| Property | Type | Description |
|----------|------|-------------|
| recipients | string \| string[] | Primary recipient(s) |
| subject | string | Email subject line |
| html | string \| undefined | HTML body |
| text | string \| undefined | Plain-text body |
| from | string \| undefined | Sender address (falls back to defaultSender if omitted) |
| cc | string \| string[] \| undefined | CC recipients |
| bcc | string \| string[] \| undefined | BCC recipients |
withFrom(address) returns a new value object with the sender set. If a sender is already present it is a no-op.
EmailTemplateValueObject
Created via EmailTemplateValueObject.create({ name, subject, html?, text? }). At least one of html or text is required.
| Property | Type | Description |
|----------|------|-------------|
| name | string | Unique template identifier |
| subject | string | Template subject line |
| html | string \| undefined | HTML template body |
| text | string \| undefined | Plain-text template body |
Custom Adapters
Custom email provider (e.g. SendGrid)
import { Injectable } from '@nestjs/common';
import { EmailProviderPort, EmailMessageValueObject } from '@devoven/email';
@Injectable()
export class SendGridProvider implements EmailProviderPort {
async send(message: EmailMessageValueObject): Promise<void> {
// call SendGrid SDK
}
}Pass it as templateRenderer or use TOKENS.EmailProviderPort directly. For a fully custom provider, pass it via EmailModule.register — the module currently selects the provider automatically based on the presence of smtpConfig. To supply an arbitrary provider, implement EmailProviderPort and wire it yourself by extending EmailModule or wrapping it in a feature module.
Custom template repository
import { Injectable } from '@nestjs/common';
import {
EmailTemplateRepositoryPort,
EmailTemplateValueObject,
} from '@devoven/email';
@Injectable()
export class PrismaEmailTemplateRepository implements EmailTemplateRepositoryPort {
async findByName(name: string): Promise<EmailTemplateValueObject | undefined> {
// query database
}
async register(template: EmailTemplateValueObject): Promise<void> {
// persist template
}
}Pass it to register:
EmailModule.register({
defaultSender: '[email protected]',
templateRegistry: PrismaEmailTemplateRepository,
})Custom template renderer
import { Injectable } from '@nestjs/common';
import { EmailTemplateRenderPort } from '@devoven/email';
import * as Handlebars from 'handlebars';
@Injectable()
export class HandlebarsRenderer implements EmailTemplateRenderPort {
render(template: string, variables: Record<string, unknown>): string {
return Handlebars.compile(template)(variables);
}
}EmailModule.register({
defaultSender: '[email protected]',
templateRenderer: HandlebarsRenderer,
})