Fastify plugin for email service in QAuth. This plugin provides dependency injection for email services using the factory pattern, eliminating direct process.env access.
The @qauth-labs/fastify-plugin-email plugin integrates email functionality into your Fastify application by:
- Decorating the Fastify instance with
emailServiceproperty - Using factory pattern for configuration (no direct
process.envaccess) - Providing type-safe email operations
- Supporting multiple email providers (mock, resend, smtp)
- Enabling different email configurations per Fastify instance
This library is part of the QAuth monorepo and is automatically available to other projects within the workspace.
import { emailPlugin } from '@qauth-labs/fastify-plugin-email';import Fastify from 'fastify';
import { emailPlugin } from '@qauth-labs/fastify-plugin-email';
const fastify = Fastify();
// Register the email plugin with mock provider (default)
await fastify.register(emailPlugin);
// Or explicitly specify provider
await fastify.register(emailPlugin, {
provider: 'mock',
});
// Start the server
await fastify.listen({ port: 3000 });import Fastify from 'fastify';
import { emailPlugin } from '@qauth-labs/fastify-plugin-email';
const fastify = Fastify();
// Register with service configuration
await fastify.register(emailPlugin, {
provider: 'mock',
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});
await fastify.listen({ port: 3000 });Once registered, the email service is available on the Fastify instance:
fastify.post('/auth/register', async (request, reply) => {
const { email, password } = request.body as { email: string; password: string };
// Create user
const user = await createUser({ email, password });
// Generate verification token
const { token, tokenHash } = generateVerificationToken();
// Store token hash in database
await db.emailVerificationTokens.create({
tokenHash,
userId: user.id,
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
});
// Send verification email
await fastify.emailService.sendVerificationEmail(email, token);
return reply.code(201).send({
user: { id: user.id, email: user.email },
message: 'Registration successful. Please check your email to verify your account.',
});
});await fastify.register(emailPlugin, options);Options:
interface EmailPluginOptions {
/**
* Email provider type (mock, resend, smtp)
* Default: 'mock'
*/
provider?: 'mock' | 'resend' | 'smtp';
/**
* Provider-specific configuration
* Optional - depends on provider type
*/
providerConfig?: EmailProviderConfig;
/**
* Email service configuration
* Optional - missing values will use defaults
*/
serviceConfig?: {
/**
* Default sender email address
*/
defaultFrom?: string;
/**
* Base URL for verification links
*/
baseUrl?: string;
};
}Note: Both providerConfig and serviceConfig are optional. If not provided, defaults will be used.
The plugin decorates the Fastify instance with one property:
Type: EmailService
The email service instance with methods:
sendVerificationEmail(to: string, token: string, options?: Partial<EmailOptions>): Promise<EmailResult>- Send a verification email
Example:
// Send verification email
const result = await fastify.emailService.sendVerificationEmail('user@example.com', 'token123');
if (result.success) {
console.log('Email sent:', result.messageId);
} else {
console.error('Email failed:', result.error);
}The plugin includes TypeScript type definitions. fastify.emailService is automatically typed:
import { FastifyInstance } from 'fastify';
async function myRoute(fastify: FastifyInstance) {
// TypeScript knows about fastify.emailService
await fastify.emailService.sendVerificationEmail('user@example.com', 'token123');
}The mock provider logs emails to the console and stores them in memory. Useful for development and testing:
await fastify.register(emailPlugin, {
provider: 'mock',
});Features:
- No external dependencies
- Logs emails to console (in non-test environments)
- Stores emails in memory for testing
- Always succeeds
The Resend provider sends emails via the Resend API:
await fastify.register(emailPlugin, {
provider: 'resend',
providerConfig: {
apiKey: 're_...',
fromAddress: 'noreply@example.com', // Optional, can be set per email
},
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});Features:
- Automatic retry on transient failures (network errors, rate limits)
- Idempotency key support to prevent duplicate sends
- TypeScript-first SDK
- 3,000 emails/month free tier
The SMTP provider sends emails via SMTP:
await fastify.register(emailPlugin, {
provider: 'smtp',
providerConfig: {
host: 'smtp.example.com',
port: 587,
secure: false, // true for SSL/TLS, false for STARTTLS
auth: {
user: 'user@example.com',
pass: 'password',
},
fromAddress: 'noreply@example.com', // Optional
options: {
requireTLS: true, // Optional: require TLS
ignoreTLS: false, // Optional: ignore TLS certificate errors
},
},
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});Features:
- Support for STARTTLS and direct SSL/TLS
- Configurable authentication
- Connection pooling via nodemailer
- Ideal for self-hosted deployments
The plugin accepts configuration through options. For environment-based configuration, you can extend @qauth-labs/server-config:
import { env } from '@qauth-labs/server-config';
await fastify.register(emailPlugin, {
provider: env.EMAIL_PROVIDER || 'mock',
serviceConfig: {
defaultFrom: env.EMAIL_FROM,
baseUrl: env.EMAIL_BASE_URL,
},
});Note: Email environment variables are now available in @qauth-labs/server-config via emailEnvSchema. See the server-config documentation for details.
This plugin uses the factory pattern from @qauth-labs/server-email:
- No direct
process.envaccess - Configuration is passed explicitly - Testable - Easy to inject mock configurations in tests
- Flexible - Different Fastify instances can use different configurations
// Different configurations for different instances
const productionApp = Fastify();
await productionApp.register(emailPlugin, {
provider: 'resend',
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});
const testApp = Fastify();
await testApp.register(emailPlugin, {
provider: 'mock', // Use mock for tests
});Register the email plugin after database and password plugins:
import { databasePlugin } from '@qauth-labs/fastify-plugin-db';
import { cachePlugin } from '@qauth-labs/fastify-plugin-cache';
import { passwordPlugin } from '@qauth-labs/fastify-plugin-password';
import { emailPlugin } from '@qauth-labs/fastify-plugin-email';
import { env } from '@qauth-labs/server-config';
await fastify.register(databasePlugin, {
config: {
connectionString: env.DATABASE_URL,
pool: {
max: env.DB_POOL_MAX,
min: env.DB_POOL_MIN,
idleTimeoutMillis: env.DB_POOL_IDLE_TIMEOUT,
connectionTimeoutMillis: env.DB_POOL_CONNECTION_TIMEOUT,
},
},
});
await fastify.register(cachePlugin, {
config: {
url: env.REDIS_URL,
host: env.REDIS_HOST,
port: env.REDIS_PORT,
password: env.REDIS_PASSWORD,
db: env.REDIS_DB,
maxRetriesPerRequest: env.REDIS_MAX_RETRIES,
connectTimeout: env.REDIS_CONNECTION_TIMEOUT,
commandTimeout: env.REDIS_COMMAND_TIMEOUT,
lazyConnect: true,
},
});
await fastify.register(passwordPlugin, {
hashConfig: {
memoryCost: env.PASSWORD_MEMORY_COST,
timeCost: env.PASSWORD_TIME_COST,
parallelism: env.PASSWORD_PARALLELISM,
},
validationConfig: {
minScore: env.PASSWORD_MIN_SCORE,
},
});
await fastify.register(emailPlugin, {
provider: 'mock',
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});Email operations can fail. Always handle errors:
fastify.post('/auth/register', async (request, reply) => {
try {
const result = await fastify.emailService.sendVerificationEmail(email, token);
if (!result.success) {
fastify.log.error({ error: result.error }, 'Email sending failed');
// Handle error (e.g., queue for retry, log, etc.)
}
} catch (error) {
fastify.log.error(error, 'Email service error');
reply.code(500).send({ error: 'Registration failed' });
}
});-
Register After Database/Password: Register the email plugin after database and password plugins if you need them in your routes.
-
Use Mock Provider for Tests: Use the mock provider in test environments to avoid external dependencies.
-
Error Handling: Always wrap email operations in try-catch blocks in production code.
-
Logging: The plugin logs debug information when registered. Check logs to verify plugin registration.
-
Provider Selection: Choose the appropriate provider based on your environment:
- Development:
mock - Testing:
mock - Production:
resendorsmtp
- Development:
-
Configuration: Use environment-based configuration for production deployments.
import Fastify from 'fastify';
import { databasePlugin } from '@qauth-labs/fastify-plugin-db';
import { cachePlugin } from '@qauth-labs/fastify-plugin-cache';
import { passwordPlugin } from '@qauth-labs/fastify-plugin-password';
import { emailPlugin } from '@qauth-labs/fastify-plugin-email';
import { generateVerificationToken } from '@qauth-labs/server-email';
const fastify = Fastify();
// Register plugins
await fastify.register(databasePlugin);
await fastify.register(cachePlugin);
await fastify.register(passwordPlugin);
await fastify.register(emailPlugin, {
provider: 'mock', // Use 'resend' or 'smtp' in production
serviceConfig: {
defaultFrom: 'noreply@example.com',
baseUrl: 'https://example.com',
},
});
// Registration route
fastify.post('/auth/register', async (request, reply) => {
const { email, password } = request.body as { email: string; password: string };
// Validate password strength
const strength = fastify.passwordValidator.validatePasswordStrength(password);
if (!strength.valid) {
return reply.code(422).send({
error: 'Password does not meet strength requirements',
feedback: strength.feedback,
});
}
// Hash password
const passwordHash = await fastify.passwordHasher.hashPassword(password);
// Create user
const user = await fastify.repositories.users.create({
email,
passwordHash,
// ... other fields
});
// Generate verification token
const { token, tokenHash } = generateVerificationToken();
// Store token hash in database
await fastify.repositories.emailVerificationTokens.create({
tokenHash,
userId: user.id,
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
});
// Send verification email
try {
const result = await fastify.emailService.sendVerificationEmail(email, token);
if (!result.success) {
fastify.log.error({ error: result.error }, 'Failed to send verification email');
}
} catch (error) {
fastify.log.error(error, 'Error sending verification email');
}
// Remove passwordHash from response
const { passwordHash: _, ...safeUser } = user;
return reply.code(201).send({ user: safeUser });
});
await fastify.listen({ port: 3000 });nx test fastify-plugin-emailnx lint fastify-plugin-email@qauth-labs/server-email: Email service library with factory patternfastify-plugin: Fastify plugin wrapper
@qauth-labs/server-email: Email service library with factory pattern@qauth-labs/fastify-plugin-db: Database plugin for Fastify@qauth-labs/fastify-plugin-password: Password plugin for Fastify
Apache-2.0