npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nestm/email

v0.1.0-alpha.0

Published

Provider-neutral email delivery for NestJS 12 with pluggable transports, renderers, typed DI, and testing utilities.

Readme

@nestm/email

Provider-neutral email delivery for NestJS 12. The package separates message composition from delivery, keeps provider clients application-owned, and exposes framework-neutral primitives for workers and scripts.

  • EmailModule.forRoot() and forRootAsync() with Nest dependency injection
  • Direct HTML/text messages or renderer-backed templates
  • Shared defaults for from, replyTo, and headers
  • Framework-neutral service and contracts through @nestm/email/core
  • React Email rendering through @nestm/email/renderers/react-email
  • Structural Resend and Nodemailer transports
  • An in-memory transport and local testing module through @nestm/email/testing

This package targets the NestJS 12 prerelease line and is itself published on the alpha dist-tag.

Requirements

  • Node.js 22.12 or newer
  • NestJS ^12.0.0-alpha.5 for the root entry point
  • ESM

@nestm/email/core has no NestJS imports. Provider SDKs remain application-owned: the root package does not create a Resend client, SMTP connection, or another provider client from environment variables.

Installation

pnpm add @nestm/email@alpha @nestjs/common @nestjs/core reflect-metadata rxjs

Install only the optional integration used by the application:

# React Email templates
pnpm add react react-dom react-email @react-email/render

# Resend delivery
pnpm add resend

# SMTP and other Nodemailer transports
pnpm add nodemailer
pnpm add --save-dev @types/nodemailer

NestJS 12 alpha packages may still declare prerelease sibling ranges that npm rejects under its strict resolver. If npm reports ERESOLVE for Nest's own packages, use the equivalent package-manager override or npm install --legacy-peer-deps until those upstream ranges are widened.

Quick start

Create the provider client in application infrastructure and pass its structural interface to the adapter:

import { Module } from "@nestjs/common";
import { EmailModule } from "@nestm/email";
import { createResendEmailTransport } from "@nestm/email/transports/resend";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

@Module({
	imports: [
		EmailModule.forRoot({
			transport: createResendEmailTransport(resend),
			defaults: {
				from: { name: "Acme", address: "[email protected]" },
				replyTo: "[email protected]",
				headers: { "X-Application": "api" },
			},
		}),
	],
})
export class AppModule {}

Inject EmailService and send HTML, plain text, or both:

import { Injectable } from "@nestjs/common";
import { EmailService } from "@nestm/email";

@Injectable()
export class PasswordResetService {
	constructor(private readonly email: EmailService) {}

	sendReset(address: string, resetUrl: string) {
		return this.email.send({
			to: address,
			subject: "Reset your password",
			html: `<p><a href="${resetUrl}">Reset your password</a></p>`,
			text: `Reset your password: ${resetUrl}`,
		});
	}
}

from is required after defaults and per-message options are merged. to and subject are always required. A successful send resolves to an EmailSendResult containing at least messageId and transport.

Transport versus renderer

A transport delivers a fully resolved email. It owns provider mapping, provider-result normalization, verification when supported, and optional connection cleanup. Resend and Nodemailer are transports.

A renderer turns the value passed as template into HTML and/or text. It does not deliver anything. React Email is a renderer.

Direct html/text content bypasses the configured renderer. A template requires one, and the two modes are mutually exclusive:

// Direct content: no renderer required.
await email.send({
	to: "[email protected]",
	subject: "Plain message",
	text: "Hello",
});

// Template content: rendered before the transport receives it.
await email.send({
	to: "[email protected]",
	subject: "Rendered message",
	template: templateValue,
});

This boundary keeps React elements and provider SDK request types out of the root API.

React Email renderer

Configure the optional renderer alongside any transport:

import { Button, Html, Text } from "react-email";
import { EmailModule } from "@nestm/email";
import { createReactEmailRenderer } from "@nestm/email/renderers/react-email";
import { createResendEmailTransport } from "@nestm/email/transports/resend";

function WelcomeEmail(props: { name: string; activationUrl: string }) {
	return (
		<Html>
			<Text>Welcome, {props.name}.</Text>
			<Button href={props.activationUrl}>Activate account</Button>
		</Html>
	);
}

EmailModule.forRoot({
	transport: createResendEmailTransport(resend),
	renderer: createReactEmailRenderer({
		pretty: false,
		plainText: true,
	}),
	defaults: { from: "[email protected]" },
});

await email.send({
	to: "[email protected]",
	subject: "Welcome",
	template: <WelcomeEmail name="Ada" activationUrl="https://example.com/activate" />,
});

plainText defaults to true, producing a text alternative alongside HTML. Set it to false when HTML-only output is intentional. pretty controls HTML formatting. ReactEmailRenderer is also exported for explicit construction.

The example also uses React Email's optional, application-owned component package:

pnpm add react-email

The renderer itself relies only on React, React DOM, and @react-email/render.

Resend transport

The Resend adapter accepts a structural ResendClient, so the application creates and configures the official client:

import { EmailModule } from "@nestm/email";
import { createResendEmailTransport } from "@nestm/email/transports/resend";
import { Resend } from "resend";

const client = new Resend(process.env.RESEND_API_KEY);

EmailModule.forRoot({
	transport: createResendEmailTransport(client),
	defaults: { from: "[email protected]" },
});

The adapter maps the normalized email envelope, content, headers, and attachments to the client. It does not read credentials or construct the client. The subpath exports its structural client and request/response types so compatible wrappers can be used without coupling the root package to Resend's classes. Resend does not expose an attachment-disposition field; contentId is mapped for inline references, while the portable disposition hint is ignored by this adapter.

Nodemailer transport

Create a Nodemailer transporter in the application and pass it directly:

import { EmailModule } from "@nestm/email";
import { createNodemailerEmailTransport } from "@nestm/email/transports/nodemailer";
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
	host: process.env.SMTP_HOST,
	port: 587,
	secure: false,
	auth: {
		user: process.env.SMTP_USER,
		pass: process.env.SMTP_PASSWORD,
	},
});

EmailModule.forRoot({
	transport: createNodemailerEmailTransport(transporter),
	defaults: { from: "[email protected]" },
});

The default adapter does not close the application-owned transporter. To transfer that lifecycle responsibility to EmailService, opt in explicitly:

createNodemailerEmailTransport(transporter, {
	closeTransporter: true,
});

The adapter exposes Nodemailer's verify() when the structural transporter implements it. Its subpath exports structural transporter, message, and result types; the root entry point does not expose Nodemailer types.

Configuration

EmailModule is global by default. Pass isGlobal: false when the configured service should remain local to the importing module:

EmailModule.forRoot({
	transport,
	defaults: {
		from: "[email protected]",
		replyTo: ["[email protected]", "[email protected]"],
		headers: { "X-Environment": "production" },
	},
	isGlobal: false,
});

Per-message from, replyTo, and headers override or extend configured defaults. Header names and values, subjects, addresses, and attachment metadata are validated before rendering or delivery to reject line-break injection and other malformed input. Routing headers such as From, To, Cc, Bcc, Reply-To, and Sender are reserved for the structured message fields and cannot be supplied through headers.

Register forRoot() or forRootAsync() exactly once per Nest application context. One root registration represents one default transport and renderer; multiple roots make the exported injection tokens ambiguous and may invoke lifecycle hooks more than once for a shared transport. Re-export the configured module from an application infrastructure module when several features need it.

Async configuration

forRootAsync() supports Nest's useFactory, useClass, and useExisting patterns:

import { ConfigModule, ConfigService } from "@nestjs/config";
import { EmailModule } from "@nestm/email";
import { createResendEmailTransport } from "@nestm/email/transports/resend";
import { Resend } from "resend";

EmailModule.forRootAsync({
	imports: [ConfigModule],
	inject: [ConfigService],
	useFactory: (config: ConfigService) => ({
		transport: createResendEmailTransport(new Resend(config.getOrThrow("RESEND_API_KEY"))),
		defaults: {
			from: config.getOrThrow("EMAIL_FROM"),
			replyTo: config.get("EMAIL_REPLY_TO"),
		},
	}),
});

Class and existing factories implement EmailOptionsFactory.createEmailOptions(). defineEmailConfig() is available as an identity helper for reusable typed configuration.

Addresses, headers, and attachments

Each address value represents exactly one mailbox and can be a string or named value. Use an array wherever a field needs more than one address; address-list syntax inside one string is rejected:

await email.send({
	from: { name: "Acme Billing", address: "[email protected]" },
	to: ["[email protected]", { name: "Grace", address: "[email protected]" }],
	cc: "[email protected]",
	replyTo: "[email protected]",
	subject: "Your receipt",
	headers: { "X-Correlation-ID": requestId },
	text: "Receipt attached.",
	attachments: [
		{
			filename: "receipt.txt",
			content: receipt,
			contentType: "text/plain",
		},
	],
});

Attachment content is UTF-8 text when supplied as a string; strings are never interpreted as base64. Use Uint8Array for binary bytes. Applications remain responsible for enforcing attachment size, content, and retention policies before calling the library.

Direct integration injection

Most application code should use EmailService. Low-level integrations are also injectable when provider-specific operations are unavoidable:

import {
	InjectEmailRenderer,
	InjectEmailTransport,
	type EmailRenderer,
	type EmailTransport,
} from "@nestm/email";

constructor(
	@InjectEmailTransport() readonly transport: EmailTransport,
	@InjectEmailRenderer() readonly renderer: EmailRenderer | null,
) {}

EMAIL_TRANSPORT, EMAIL_RENDERER, and EMAIL_MODULE_OPTIONS are exported for manual provider overrides.

Framework-neutral core

Workers and scripts can use the service and contracts without NestJS:

import { EmailService, type EmailTransport } from "@nestm/email/core";

declare const transport: EmailTransport;

const email = new EmailService({
	transport,
	defaults: { from: "[email protected]" },
});

await email.send({
	to: "[email protected]",
	subject: "Background job complete",
	text: "Your export is ready.",
});

await email.close();

The core entry point exports EmailService, EmailTransport, EmailRenderer, message and address types, normalization helpers, and the package error model. It does not import NestJS or any concrete provider SDK.

Lifecycle

EmailService.close() is idempotent and invokes the configured transport's optional close() method. Nest calls the same path through onApplicationShutdown() when the application context shuts down. Calls to send() or verify() after closure fail with EmailErrorCode.CLOSED. Concurrent close calls await the same operation. Its result is retained, so a failed cleanup is reported consistently and is not retried implicitly.

await email.verify() delegates to the transport's optional verify() hook; it is a no-op when the transport has no verification operation. Verification is never performed automatically during module bootstrap.

Provider ownership is adapter-specific:

  • Resend has no connection resource for the adapter to close.
  • Nodemailer remains application-owned unless closeTransporter: true is set.
  • Custom transports decide what their close() implementation owns.
  • Renderers have no lifecycle hook.

Applications using long-lived Nest processes should enable and exercise their normal graceful-shutdown path. Framework-neutral consumers should call email.close() explicitly.

Errors

Validation, rendering, verification, lifecycle, and delivery failures are reported as EmailError with one of these codes:

  • INVALID_ARGUMENT
  • NOT_CONFIGURED
  • RENDER_FAILED
  • DELIVERY_FAILED
  • VERIFY_FAILED
  • CLOSED

isEmailError() works across duplicate package copies. Provider failures are available as cause; avoid returning or logging raw causes when provider responses may contain addresses, message content, or credential-bearing URLs. The library does not retry sends automatically because an ambiguous provider failure may already have delivered the message.

Testing

Use the in-memory transport to inspect normalized, rendered messages without a network request:

import { Test } from "@nestjs/testing";
import { EmailService } from "@nestm/email";
import { createEmailTestingModule, createMemoryEmailTransport } from "@nestm/email/testing";

const transport = createMemoryEmailTransport();
const moduleRef = await Test.createTestingModule({
	imports: [
		createEmailTestingModule({
			transport,
			defaults: { from: "[email protected]" },
		}),
	],
}).compile();

await moduleRef.get(EmailService).send({
	to: "[email protected]",
	subject: "Test",
	text: "Recorded, not delivered.",
});

expect(transport.messages).toHaveLength(1);
expect(transport.messages[0]?.subject).toBe("Test");

await moduleRef.close();

Testing modules are local by default to prevent state from leaking between tests. MemoryEmailTransport.clear() removes recorded messages. A custom renderer can be passed to createEmailTestingModule() for template tests.

Package entry points

| Entry point | Purpose | Optional runtime | | ------------------------------------ | -------------------------------------------- | ---------------------------------- | | @nestm/email | NestJS module, service, tokens, and core API | NestJS | | @nestm/email/core | Framework-neutral service and contracts | None | | @nestm/email/renderers/react-email | React element to HTML/text renderer | React Email | | @nestm/email/transports/resend | Structural Resend delivery adapter | Application Resend client | | @nestm/email/transports/nodemailer | Structural Nodemailer delivery adapter | Application Nodemailer transporter | | @nestm/email/testing | In-memory transport and testing module | NestJS for the module helper |

Every optional subpath is independently importable. Importing the root or core entry point does not load React Email, Resend, or Nodemailer.

Non-goals

  • Template storage, discovery, localization, or versioning
  • Queues, scheduling, retries, deduplication, or delivery webhooks
  • Contact lists, unsubscribe management, or campaign analytics
  • Provider-client construction or ambient credential lookup
  • Sanitizing application-provided HTML or deciding whether user content is safe

Compose those concerns in the application or dedicated infrastructure around the small renderer/transport boundary.

License

BSD-3-Clause