@igniter-js/mail
v0.1.14
Published
Type-safe email library for Igniter.js with React Email templates and multiple provider adapters
Maintainers
Readme
@igniter-js/mail
Type-safe email library for Igniter.js applications with React Email templates and multiple provider adapters. Send transactional emails with confidence using compile-time type safety and runtime validation.
Features
- ✅ Type-Safe Templates - Full TypeScript inference with template payload validation
- ✅ React Email - Build beautiful emails with React components
- ✅ Multiple Providers - Resend, Postmark, SendGrid, SMTP
- ✅ Mock Adapter - In-memory adapter for unit testing
- ✅ Schema Validation - Runtime validation with StandardSchema (Zod 3.23+ or any StandardSchemaV1-compatible lib)
- ✅ Telemetry Ready - Optional integration with
@igniter-js/telemetry - ✅ Queue Integration - Schedule emails with BullMQ or custom queues
- ✅ Lifecycle Hooks - React to send events (started, success, error)
- ✅ Builder Pattern - Fluent API for configuration
- ✅ Server-First - Built for Node.js, Bun, Deno (no browser dependencies)
Installation
# npm
npm install @igniter-js/mail @react-email/components react
# pnpm
pnpm add @igniter-js/mail @react-email/components react
# yarn
yarn add @igniter-js/mail @react-email/components react
# bun
bun add @igniter-js/mail @react-email/components reactProvider Dependencies
Install the adapter you need:
Resend:
npm install resendSMTP:
npm install nodemailer @types/nodemailerOptional Dependencies
Telemetry:
npm install @igniter-js/telemetryValidation (StandardSchemaV1-compatible):
npm install zodQuick Start
1. Create Email Templates
Use React Email components to build your templates:
// src/emails/welcome.tsx
import { Button, Html, Text } from '@react-email/components'
export interface WelcomeEmailProps {
name: string
verifyUrl: string
}
export function WelcomeEmail({ name, verifyUrl }: WelcomeEmailProps) {
return (
<Html>
<Text>Hi {name},</Text>
<Text>Welcome to our platform! Click below to verify your email:</Text>
<Button href={verifyUrl}>Verify Email</Button>
</Html>
)
}2. Initialize Mail Service
import { IgniterMail } from '@igniter-js/mail'
import { z } from 'zod'
import { WelcomeEmail } from './emails/welcome'
// Create mail instance with builder
export const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.RESEND_API_KEY!)
.addTemplate('welcome', {
subject: 'Welcome to Our Platform',
schema: z.object({
name: z.string(),
verifyUrl: z.string().url(),
}),
render: WelcomeEmail,
})
.build()3. Send Emails
// Send immediately
await mail.send({
to: '[email protected]',
template: 'welcome',
data: {
name: 'John Doe',
verifyUrl: 'https://example.com/verify?token=abc123',
},
})
// Schedule for later (requires withQueue configured)
await mail.schedule(
{
to: '[email protected]',
template: 'welcome',
data: {
name: 'John Doe',
verifyUrl: 'https://example.com/verify?token=abc123',
},
},
new Date(Date.now() + 24 * 60 * 60 * 1000) // Send in 24 hours
)Core Concepts
Templates
Templates combine React Email components with schema validation:
import { IgniterMail } from '@igniter-js/mail'
import { z } from 'zod'
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.IGNITER_MAIL_SECRET!)
.addTemplate('resetPassword', {
subject: 'Reset Your Password',
schema: z.object({
name: z.string(),
resetLink: z.string().url(),
expiresAt: z.date(),
}),
render: ({ name, resetLink, expiresAt }) => (
<Html>
<Text>Hi {name},</Text>
<Text>Click below to reset your password:</Text>
<Button href={resetLink}>Reset Password</Button>
<Text>This link expires at {expiresAt.toLocaleString()}</Text>
</Html>
),
})
.build()Template Registry
Access template metadata and render previews without sending:
const templates = await mail.templates.list()
const template = await mail.templates.get('resetPassword')
const preview = await mail.templates.render('resetPassword', {
name: 'Jane',
resetLink: 'https://example.com/reset',
expiresAt: new Date(),
})Type Safety
The library provides end-to-end type safety:
// ✅ TypeScript knows 'welcome' template exists
await mail.send({
to: '[email protected]',
template: 'welcome',
data: {
name: 'John Doe',
verifyUrl: 'https://example.com/verify',
},
})
// ❌ TypeScript error: unknown template
await mail.send({
to: '[email protected]',
template: 'unknown', // Error: Type '"unknown"' is not assignable to type '"welcome"'
data: {},
})
// ❌ TypeScript error: invalid data shape
await mail.send({
to: '[email protected]',
template: 'welcome',
data: {
invalidProp: true, // Error: Object literal may only specify known properties
},
})Schema Validation
Templates support StandardSchemaV1 for runtime validation (Zod 3.23+ or any compatible library):
import { z } from 'zod'
import { IgniterMail } from '@igniter-js/mail'
const mail = IgniterMail.create()
.addTemplate('notification', {
subject: 'New Notification',
schema: z.object({
message: z.string().min(1).max(500),
priority: z.enum(['low', 'medium', 'high']),
}),
render: ({ message, priority }) => (
<Html>
<Text>Priority: {priority}</Text>
<Text>{message}</Text>
</Html>
),
})
.build()
// ✅ Valid data
await mail.send({
to: '[email protected]',
template: 'notification',
data: {
message: 'Your order has shipped!',
priority: 'high',
},
})
// ❌ Runtime validation error
await mail.send({
to: '[email protected]',
template: 'notification',
data: {
message: '', // Error: String must contain at least 1 character(s)
priority: 'urgent', // Error: Invalid enum value
},
})Adapters
Resend
import { IgniterMail } from '@igniter-js/mail'
import { ResendMailAdapterBuilder } from '@igniter-js/mail'
// Using builder shorthand
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.RESEND_API_KEY!)
.build()
// Or using adapter builder
const adapter = ResendMailAdapterBuilder.create()
.withSecret(process.env.RESEND_API_KEY!)
.withFrom('[email protected]')
.build()
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter(adapter)
.build()Postmark
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('postmark', process.env.POSTMARK_SERVER_TOKEN!)
.build()SendGrid
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('sendgrid', process.env.SENDGRID_API_KEY!)
.build()SMTP
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('smtp', JSON.stringify({
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
}))
.build()Mock Adapter
For unit tests:
import { MockMailAdapter } from '@igniter-js/mail/adapters'
const adapter = MockMailAdapter.create()
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter(adapter)
.build()
// Send email
await mail.send({ ... })
// Verify in tests
expect(adapter.sent).toHaveLength(1)
expect(adapter.sent[0].to).toBe('[email protected]')
expect(adapter.sent[0].html).toContain('Welcome')Queue Integration
Integrate with BullMQ or custom job queues for async email delivery:
import { IgniterMail } from '@igniter-js/mail'
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq'
const queueAdapter = createBullMQAdapter({
connection: {
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
},
})
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.RESEND_API_KEY!)
.withQueue(queueAdapter, {
queue: 'mail',
job: 'send',
attempts: 3,
removeOnComplete: true,
})
.addTemplate('welcome', { ... })
.build()
// This now enqueues the email instead of sending immediately
await mail.schedule(
{
to: '[email protected]',
template: 'welcome',
data: { name: 'John' },
},
new Date(Date.now() + 60000) // Send in 1 minute
)Lifecycle Hooks
React to email sending events:
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.RESEND_API_KEY!)
.onSendStarted(async (params) => {
console.log('Starting to send email:', params.template)
})
.onSendSuccess(async (params) => {
console.log('Email sent successfully:', params.template)
// Log to analytics, update database, etc.
})
.onSendError(async (params, error) => {
console.error('Failed to send email:', error)
// Log error, send alert, etc.
})
.build()Telemetry Integration
Use @igniter-js/telemetry to capture mail events with a typed schema:
import { IgniterTelemetry } from '@igniter-js/telemetry'
import { IgniterMailTelemetryEvents } from '@igniter-js/mail/telemetry'
const telemetry = IgniterTelemetry.create()
.withService('my-api')
.addEvents(IgniterMailTelemetryEvents)
.build()
const mail = IgniterMail.create()
.withTelemetry(telemetry)
// ...
.build()API Reference
IgniterMail
Main mail client created by the builder.
Methods
send(params)
Sends an email immediately.
await mail.send({
to: string
template: TemplateKey
data: TemplatePayload
subject?: string // Optional subject override
})schedule(params, date)
Schedules an email for a future date. Requires a queue adapter.
await mail.schedule(
{
to: string
template: TemplateKey
data: TemplatePayload
subject?: string
},
date: Date
)templates.list()
Lists template metadata available in the registry.
const templates = await mail.templates.list()templates.get(id)
Gets a single template metadata entry.
const template = await mail.templates.get('welcome')templates.render(id, variables?)
Renders a template to HTML and plain text without sending.
const preview = await mail.templates.render('welcome', {
name: 'User',
})IgniterMailBuilder
Fluent API for configuring the mail service.
Methods
create()
Creates a new builder instance.
const builder = IgniterMail.create()withFrom(from)
Sets the default FROM address.
builder.withFrom('[email protected]')withAdapter(adapter)
Sets the mail adapter (instance or provider + secret).
// With provider string + secret
builder.withAdapter('resend', process.env.RESEND_API_KEY!)
// With adapter instance
builder.withAdapter(adapterInstance)withLogger(logger)
Attaches a logger for debugging.
builder.withLogger(logger)withQueue(adapter, options?)
Enables queue-based delivery.
builder.withQueue(queueAdapter, {
queue: 'mail',
job: 'send',
attempts: 3,
})addTemplate(key, template)
Registers an email template.
builder.addTemplate('welcome', {
subject: 'Welcome',
schema: z.object({ name: z.string() }),
render: ({ name }) => <Html>...</Html>,
})onSendStarted(hook)
Registers a hook for send start events.
builder.onSendStarted(async (params) => {
console.log('Sending:', params.template)
})onSendSuccess(hook)
Registers a hook for send success events.
builder.onSendSuccess(async (params) => {
console.log('Sent:', params.template)
})onSendError(hook)
Registers a hook for send error events.
builder.onSendError(async (params, error) => {
console.error('Failed:', error)
})build()
Builds the mail instance.
const mail = builder.build()Error Handling
All errors are instances of IgniterMailError with stable error codes:
try {
await mail.send({ ... })
} catch (error) {
if (error instanceof IgniterMailError) {
console.error('Code:', error.code)
console.error('Metadata:', error.metadata)
}
}Common Error Codes:
MAIL_PROVIDER_ADAPTER_REQUIRED- No adapter configuredMAIL_PROVIDER_TEMPLATES_REQUIRED- No templates registeredMAIL_PROVIDER_TEMPLATE_NOT_FOUND- Template key doesn't existMAIL_PROVIDER_TEMPLATE_DATA_INVALID- Schema validation failedMAIL_PROVIDER_SEND_FAILED- Failed to send emailMAIL_PROVIDER_SCHEDULE_DATE_INVALID- Schedule date is in the pastMAIL_PROVIDER_SCHEDULE_QUEUE_NOT_CONFIGURED- Queue adapter is required for schedulingMAIL_PROVIDER_SCHEDULE_FAILED- Failed to schedule emailMAIL_ADAPTER_CONFIGURATION_INVALID- Adapter configuration error
TypeScript Support
Full TypeScript support with compile-time type inference:
const mail = IgniterMail.create()
.addTemplate('welcome', {
subject: 'Welcome',
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
render: (props) => <Html>...</Html>,
})
.build()
// Type inference works!
type Templates = typeof mail.$Infer.Templates // 'welcome'
type WelcomePayload = typeof mail.$Infer.Payloads['welcome'] // { name: string; email: string }
type SendInput = typeof mail.$Infer.SendInput // Union of all send paramsBest Practices
- Centralize Templates - Define all templates in one place for consistency
- Use Schema Validation - Always provide schemas for runtime safety
- Leverage Hooks - Use hooks for logging, analytics, and error tracking
- Queue Heavy Loads - Use queue integration for high-volume sending
- Mock Adapter First - Use MockMailAdapter in unit tests before deploying
- Environment Variables - Store API keys and secrets in environment variables
- Preview Emails - Use React Email's preview feature during development
Examples
Password Reset Email
import { Button, Html, Text } from '@react-email/components'
import { IgniterMail } from '@igniter-js/mail'
import { z } from 'zod'
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('resend', process.env.RESEND_API_KEY!)
.addTemplate('resetPassword', {
subject: 'Reset Your Password',
schema: z.object({
name: z.string(),
resetLink: z.string().url(),
expiresIn: z.number(),
}),
render: ({ name, resetLink, expiresIn }) => (
<Html>
<Text>Hi {name},</Text>
<Text>You requested to reset your password.</Text>
<Button href={resetLink}>Reset Password</Button>
<Text>This link expires in {expiresIn} minutes.</Text>
</Html>
),
})
.build()
// Usage
await mail.send({
to: '[email protected]',
template: 'resetPassword',
data: {
name: 'John Doe',
resetLink: 'https://example.com/reset?token=abc',
expiresIn: 15,
},
})Order Confirmation with Queue
import { IgniterMail } from '@igniter-js/mail'
import { createBullMQAdapter } from '@igniter-js/adapter-bullmq'
const queueAdapter = createBullMQAdapter({ ... })
const mail = IgniterMail.create()
.withFrom('[email protected]')
.withAdapter('postmark', process.env.POSTMARK_TOKEN!)
.withQueue(queueAdapter, {
queue: 'mail',
job: 'send',
attempts: 5,
priority: 10,
})
.addTemplate('orderConfirmation', { ... })
.build()
// Sends via queue
await mail.send({
to: '[email protected]',
template: 'orderConfirmation',
data: { orderNumber: '12345', items: [...] },
})Contributing
Contributions are welcome! Please see the main CONTRIBUTING.md for details.
License
MIT License - see LICENSE for details.
Links
- Documentation: https://igniterjs.com/docs/mail
- GitHub: https://github.com/felipebarcelospro/igniter-js
- NPM: https://www.npmjs.com/package/@igniter-js/mail
- Issues: https://github.com/felipebarcelospro/igniter-js/issues
- React Email: https://react.email
