Skip to main content

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:
npm install axios resend
# or
yarn add axios resend
For TypeScript projects:
npm install -D @types/node typescript
# or
yarn add -D @types/node typescript

TypeScript Setup

Type Definitions

Create type definitions for better development experience:
// 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:
// 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:
// 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

// 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

// 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

// 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

// 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:
// 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

// 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:
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:
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:
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:
// 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

Need help? Contact [email protected]