Sending an email via Senderr using JavaScript
This guide shows you how to integrate Senderr email templates into your JavaScript and Node.js applications, with examples for both server-side and client-side integrations.Prerequisites
Before you begin, make sure you have:- Node.js 16 or higher
- A Senderr account with templates in your library
- A Senderr API key (generate one here)
- A Resend account for sending emails (sign up here)
Installation
Install the required packages:Copy
npm install axios resend
# or
yarn add axios resend
Copy
npm install -D @types/node typescript
# or
yarn add -D @types/node typescript
TypeScript Setup
Type Definitions
Create type definitions for better development experience:Copy
// types/senderr.ts
export interface Template {
id: string;
name: string;
description: string;
category: string;
price: number;
is_public: boolean;
created_at: string;
updated_at: string;
}
export interface TemplateSchema {
required: string[];
optional: string[];
properties: {
[key: string]: {
type: string;
description?: string;
default?: any;
};
};
}
export interface RenderRequest {
variables: Record<string, any>;
format?: 'html' | 'react';
}
export interface RenderResponse {
html: string;
subject?: string;
success: boolean;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
Senderr Client
Create a TypeScript client for the Senderr API:Copy
// lib/senderr-client.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import { Template, TemplateSchema, RenderRequest, RenderResponse, ApiResponse } from '../types/senderr';
export class SenderrClient {
private client: AxiosInstance;
constructor(apiKey?: string) {
const key = apiKey || process.env.SENDERR_API_KEY;
if (!key) {
throw new Error('Senderr API key is required');
}
this.client = axios.create({
baseURL: 'https://senderr.dev/api/v1',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.data) {
throw new Error(error.response.data as string);
}
throw error;
}
);
}
async getLibrary(): Promise<Template[]> {
try {
const response = await this.client.get<ApiResponse<{ templates: Template[] }>>('/library');
return response.data.data?.templates || [];
} catch (error) {
throw new Error(`Failed to fetch library: ${error}`);
}
}
async getTemplateSchema(templateId: string): Promise<TemplateSchema> {
try {
const response = await this.client.get<ApiResponse<TemplateSchema>>(`/templates/${templateId}/schema`);
if (!response.data.data) {
throw new Error('Template schema not found');
}
return response.data.data;
} catch (error) {
throw new Error(`Failed to fetch template schema: ${error}`);
}
}
async renderTemplate(templateId: string, request: RenderRequest): Promise<RenderResponse> {
try {
const response = await this.client.post<ApiResponse<RenderResponse>>(
`/templates/${templateId}/render`,
request
);
if (!response.data.data) {
throw new Error('Template rendering failed');
}
return response.data.data;
} catch (error) {
throw new Error(`Failed to render template: ${error}`);
}
}
async findTemplateByName(name: string): Promise<Template | null> {
const templates = await this.getLibrary();
return templates.find(t => t.name.toLowerCase().includes(name.toLowerCase())) || null;
}
}
Email Service
Create an email service using Resend:Copy
// lib/email-service.ts
import { Resend } from 'resend';
export interface SendEmailOptions {
to: string | string[];
subject: string;
html: string;
from?: string;
replyTo?: string;
cc?: string[];
bcc?: string[];
}
export interface SendEmailResult {
success: boolean;
id?: string;
error?: string;
}
export class EmailService {
private resend: Resend;
private defaultFrom: string;
constructor(apiKey?: string, defaultFrom = '[email protected]') {
const key = apiKey || process.env.RESEND_API_KEY;
if (!key) {
throw new Error('Resend API key is required');
}
this.resend = new Resend(key);
this.defaultFrom = defaultFrom;
}
async sendEmail(options: SendEmailOptions): Promise<SendEmailResult> {
try {
const result = await this.resend.emails.send({
from: options.from || this.defaultFrom,
to: Array.isArray(options.to) ? options.to : [options.to],
subject: options.subject,
html: options.html,
reply_to: options.replyTo,
cc: options.cc,
bcc: options.bcc,
});
return {
success: true,
id: result.data?.id,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async sendBulkEmails(emails: SendEmailOptions[]): Promise<SendEmailResult[]> {
// Send emails concurrently with rate limiting
const results: SendEmailResult[] = [];
const batchSize = 10; // Adjust based on Resend limits
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
const batchPromises = batch.map(email => this.sendEmail(email));
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.push(result.value);
} else {
results.push({
success: false,
error: result.reason?.message || 'Failed to send email',
});
}
});
// Add delay between batches to respect rate limits
if (i + batchSize < emails.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
}
Complete Example: Welcome Email Service
Copy
// services/welcome-email.ts
import { SenderrClient } from '../lib/senderr-client';
import { EmailService, SendEmailOptions } from '../lib/email-service';
export interface User {
email: string;
firstName: string;
lastName: string;
company?: string;
signupDate?: Date;
}
export class WelcomeEmailService {
private senderr: SenderrClient;
private emailService: EmailService;
private welcomeTemplateId: string;
constructor(welcomeTemplateId: string) {
this.senderr = new SenderrClient();
this.emailService = new EmailService();
this.welcomeTemplateId = welcomeTemplateId;
}
async sendWelcomeEmail(user: User): Promise<{ success: boolean; error?: string }> {
try {
// Validate template exists and get schema
const schema = await this.senderr.getTemplateSchema(this.welcomeTemplateId);
// Prepare variables
const variables = {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
companyName: user.company || 'there',
signupDate: user.signupDate?.toLocaleDateString() || new Date().toLocaleDateString(),
dashboardUrl: 'https://yourapp.com/dashboard',
supportEmail: '[email protected]',
};
// Validate required variables
this.validateVariables(schema, variables);
// Render template
const rendered = await this.senderr.renderTemplate(this.welcomeTemplateId, {
variables,
format: 'html',
});
// Send email
const emailResult = await this.emailService.sendEmail({
to: user.email,
subject: `Welcome to our platform, ${user.firstName}!`,
html: rendered.html,
});
return emailResult;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
private validateVariables(schema: any, variables: Record<string, any>): void {
const missing = schema.required?.filter((key: string) => !(key in variables)) || [];
if (missing.length > 0) {
throw new Error(`Missing required variables: ${missing.join(', ')}`);
}
}
async sendBulkWelcomeEmails(users: User[]): Promise<Array<{ user: User; result: { success: boolean; error?: string } }>> {
const results = await Promise.allSettled(
users.map(async (user) => ({
user,
result: await this.sendWelcomeEmail(user),
}))
);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
user: users[index],
result: {
success: false,
error: result.reason?.message || 'Failed to send welcome email',
},
};
}
});
}
}
// Usage
async function main() {
const welcomeService = new WelcomeEmailService('your_welcome_template_id');
const newUser: User = {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
company: 'Acme Corp',
signupDate: new Date(),
};
const result = await welcomeService.sendWelcomeEmail(newUser);
if (result.success) {
console.log('Welcome email sent successfully!');
} else {
console.error('Failed to send welcome email:', result.error);
}
}
Express.js Integration
Copy
// routes/email.ts
import express from 'express';
import { body, validationResult } from 'express-validator';
import { SenderrClient } from '../lib/senderr-client';
import { EmailService } from '../lib/email-service';
const router = express.Router();
const senderr = new SenderrClient();
const emailService = new EmailService();
// Send email endpoint
router.post('/send',
[
body('templateId').notEmpty().withMessage('Template ID is required'),
body('to').isEmail().withMessage('Valid email address is required'),
body('variables').isObject().withMessage('Variables must be an object'),
body('subject').optional().isString(),
],
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, errors: errors.array() });
}
const { templateId, to, variables, subject } = req.body;
// Render template
const rendered = await senderr.renderTemplate(templateId, {
variables,
format: 'html',
});
// Send email
const result = await emailService.sendEmail({
to,
subject: subject || 'Email from Senderr',
html: rendered.html,
});
res.json(result);
} catch (error) {
console.error('Email sending error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
});
}
}
);
// Get templates endpoint
router.get('/templates', async (req, res) => {
try {
const templates = await senderr.getLibrary();
res.json({ success: true, templates });
} catch (error) {
console.error('Template fetch error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
});
}
});
// Get template schema endpoint
router.get('/templates/:id/schema', async (req, res) => {
try {
const { id } = req.params;
const schema = await senderr.getTemplateSchema(id);
res.json({ success: true, schema });
} catch (error) {
console.error('Schema fetch error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
});
}
});
export default router;
// app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import emailRoutes from './routes/email';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/email', emailRoutes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Next.js Integration
API Route
Copy
// pages/api/send-email.ts (Pages Router)
// or app/api/send-email/route.ts (App Router)
import { NextRequest, NextResponse } from 'next/server';
import { SenderrClient } from '@/lib/senderr-client';
import { EmailService } from '@/lib/email-service';
const senderr = new SenderrClient();
const emailService = new EmailService();
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { templateId, to, variables, subject } = body;
// Validate input
if (!templateId || !to || !variables) {
return NextResponse.json(
{ success: false, error: 'Missing required fields' },
{ status: 400 }
);
}
// Render template
const rendered = await senderr.renderTemplate(templateId, {
variables,
format: 'html',
});
// Send email
const result = await emailService.sendEmail({
to,
subject: subject || 'Email from Senderr',
html: rendered.html,
});
return NextResponse.json(result);
} catch (error) {
console.error('Email API error:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error'
},
{ status: 500 }
);
}
}
React Hook
Copy
// hooks/use-senderr.ts
import { useState, useEffect } from 'react';
import { Template } from '@/types/senderr';
export function useSenderr() {
const [templates, setTemplates] = useState<Template[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchTemplates = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/templates');
const data = await response.json();
if (data.success) {
setTemplates(data.templates);
} else {
setError(data.error || 'Failed to fetch templates');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
const sendEmail = async (templateId: string, to: string, variables: Record<string, any>, subject?: string) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId, to, variables, subject }),
});
const data = await response.json();
if (!data.success) {
setError(data.error || 'Failed to send email');
return false;
}
return true;
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
return false;
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTemplates();
}, []);
return {
templates,
loading,
error,
sendEmail,
refetchTemplates: fetchTemplates,
};
}
JavaScript (Plain) Examples
For plain JavaScript projects without TypeScript:Copy
// lib/senderr-client.js
const axios = require('axios');
class SenderrClient {
constructor(apiKey) {
this.apiKey = apiKey || process.env.SENDERR_API_KEY;
if (!this.apiKey) {
throw new Error('Senderr API key is required');
}
this.client = axios.create({
baseURL: 'https://senderr.dev/api/v1',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
}
async getLibrary() {
try {
const response = await this.client.get('/library');
return response.data.templates || [];
} catch (error) {
throw new Error(`Failed to fetch library: ${error.message}`);
}
}
async renderTemplate(templateId, variables, format = 'html') {
try {
const response = await this.client.post(`/templates/${templateId}/render`, {
variables,
format,
});
return response.data;
} catch (error) {
throw new Error(`Failed to render template: ${error.message}`);
}
}
}
module.exports = { SenderrClient };
// Usage
const { SenderrClient } = require('./lib/senderr-client');
const { Resend } = require('resend');
async function sendWelcomeEmail(userEmail, firstName) {
const senderr = new SenderrClient();
const resend = new Resend(process.env.RESEND_API_KEY);
try {
// Render template
const rendered = await senderr.renderTemplate('welcome_template_id', {
firstName,
email: userEmail,
dashboardUrl: 'https://yourapp.com/dashboard',
});
// Send email
const result = await resend.emails.send({
from: '[email protected]',
to: userEmail,
subject: `Welcome, ${firstName}!`,
html: rendered.html,
});
console.log('Email sent:', result.data.id);
return true;
} catch (error) {
console.error('Failed to send email:', error.message);
return false;
}
}
// Call the function
sendWelcomeEmail('[email protected]', 'John');
Testing
Copy
// tests/senderr-client.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import { SenderrClient } from '../lib/senderr-client';
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
describe('SenderrClient', () => {
let client: SenderrClient;
beforeEach(() => {
vi.clearAllMocks();
mockedAxios.create.mockReturnValue(mockedAxios as any);
client = new SenderrClient('test-api-key');
});
it('should fetch templates successfully', async () => {
const mockTemplates = [{ id: '1', name: 'Test Template' }];
mockedAxios.get.mockResolvedValue({
data: { data: { templates: mockTemplates } }
});
const templates = await client.getLibrary();
expect(templates).toEqual(mockTemplates);
expect(mockedAxios.get).toHaveBeenCalledWith('/library');
});
it('should render template successfully', async () => {
const mockRendered = { html: '<h1>Hello John</h1>' };
mockedAxios.post.mockResolvedValue({
data: { data: mockRendered }
});
const result = await client.renderTemplate('template-id', { name: 'John' });
expect(result).toEqual(mockRendered);
expect(mockedAxios.post).toHaveBeenCalledWith('/templates/template-id/render', {
variables: { name: 'John' },
format: 'html'
});
});
});
Best Practices
1. Error Handling
Always implement comprehensive error handling:Copy
class ApiError extends Error {
constructor(message: string, public statusCode?: number) {
super(message);
this.name = 'ApiError';
}
}
// In your client methods
try {
const response = await this.client.get('/endpoint');
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new ApiError(
error.response?.data?.message || error.message,
error.response?.status
);
}
throw error;
}
2. Rate Limiting & Retries
Implement retry logic with exponential backoff:Copy
async function withRetry<T>(
operation: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
3. Caching
Implement caching for frequently accessed data:Copy
class CachedSenderrClient extends SenderrClient {
private cache = new Map<string, { data: any; expiry: number }>();
private cacheTimeout = 5 * 60 * 1000; // 5 minutes
async getTemplateSchema(templateId: string) {
const cacheKey = `schema:${templateId}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
const schema = await super.getTemplateSchema(templateId);
this.cache.set(cacheKey, {
data: schema,
expiry: Date.now() + this.cacheTimeout
});
return schema;
}
}
4. Environment Configuration
Use proper environment configuration:Copy
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
SENDERR_API_KEY: z.string(),
RESEND_API_KEY: z.string(),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = envSchema.parse(process.env);
Next Steps
- Check out n8n integration
- Learn about Python integration
- Explore the API reference
- Browse available templates