Next.js Integration

Learn how to integrate Shoutbox with Next.js using both the App Router and Pages Router approaches.

Installation

npm install shoutboxnet

App Router Integration

API Route Handler (Route Handlers)

// app/api/send-email/route.ts
import Shoutbox from "shoutboxnet";

export async function POST(request: Request) {
  try {
    const shoutbox = new Shoutbox({
      apiKey: process.env.SHOUTBOX_API_KEY
    });

    const { to, subject, html } = await request.json();

    const result = await shoutbox.sendEmail({
      from: "[email protected]",
      to,
      subject,
      html,
    });

    return Response.json({ success: true, result });
  } catch (error) {
    return Response.json(
      { success: false, error: error.message },
      { status: 500 }
    );
  }
}

Server Component Usage

// app/contact/page.tsx
import Shoutbox from "shoutboxnet";

async function ContactPage() {
  async function sendEmail(formData: FormData) {
    'use server';
    
    const shoutbox = new Shoutbox({
      apiKey: process.env.SHOUTBOX_API_KEY
    });

    try {
      await shoutbox.sendEmail({
        from: "[email protected]",
        to: formData.get('email') as string,
        subject: "Contact Form Submission",
        html: formData.get('message') as string,
      });
    } catch (error) {
      console.error('Failed to send email:', error);
      throw new Error('Failed to send email');
    }
  }

  return (
    <form action={sendEmail}>
      <input type="email" name="email" required />
      <textarea name="message" required></textarea>
      <button type="submit">Send</button>
    </form>
  );
}

export default ContactPage;

Client Component Usage

// app/components/EmailForm.tsx
'use client';

import { useState } from 'react';

export default function EmailForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus('loading');

    const formData = new FormData(event.currentTarget);

    try {
      const response = await fetch('/api/send-email', {
        method: 'POST',
        body: JSON.stringify({
          to: formData.get('email'),
          subject: 'Contact Form',
          html: formData.get('message'),
        }),
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) throw new Error('Failed to send email');
      
      setStatus('success');
    } catch (error) {
      console.error('Error sending email:', error);
      setStatus('error');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" required />
      <textarea name="message" required></textarea>
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
      {status === 'success' && <p>Email sent successfully!</p>}
      {status === 'error' && <p>Failed to send email. Please try again.</p>}
    </form>
  );
}

Pages Router Integration

API Route

// pages/api/send-email.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import Shoutbox from "shoutboxnet";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  try {
    const shoutbox = new Shoutbox({
      apiKey: process.env.SHOUTBOX_API_KEY
    });

    const { to, subject, html } = req.body;

    const result = await shoutbox.sendEmail({
      from: "[email protected]",
      to,
      subject,
      html,
    });

    res.status(200).json({ success: true, result });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
}

Page Component Usage

// pages/contact.tsx
import { useState } from 'react';
import type { NextPage } from 'next';

const ContactPage: NextPage = () => {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus('loading');

    const formData = new FormData(event.currentTarget);

    try {
      const response = await fetch('/api/send-email', {
        method: 'POST',
        body: JSON.stringify({
          to: formData.get('email'),
          subject: 'Contact Form',
          html: formData.get('message'),
        }),
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) throw new Error('Failed to send email');
      
      setStatus('success');
    } catch (error) {
      console.error('Error sending email:', error);
      setStatus('error');
    }
  }

  return (
    <div>
      <h1>Contact Us</h1>
      <form onSubmit={handleSubmit}>
        <input type="email" name="email" required />
        <textarea name="message" required></textarea>
        <button type="submit" disabled={status === 'loading'}>
          {status === 'loading' ? 'Sending...' : 'Send'}
        </button>
        {status === 'success' && <p>Email sent successfully!</p>}
        {status === 'error' && <p>Failed to send email. Please try again.</p>}
      </form>
    </div>
  );
};

export default ContactPage;

Environment Variables

Create a .env.local file in your project root:

SHOUTBOX_API_KEY=your_api_key_here

Best Practices

Security

  1. Always keep your API key in environment variables
  2. Validate email inputs on both client and server
  3. Implement rate limiting for your API routes
  4. Use appropriate CORS settings
  5. Validate request bodies

Error Handling

// Reusable error handler
const handleEmailError = (error: unknown) => {
  if (error instanceof ShoutboxError) {
    console.error('Shoutbox API Error:', {
      message: error.message,
      statusCode: error.statusCode,
      code: error.code,
    });
    // Handle specific error codes
    switch (error.code) {
      case 'rate_limit_exceeded':
        return 'Too many requests. Please try again later.';
      case 'invalid_recipient':
        return 'Invalid email address provided.';
      default:
        return 'An error occurred while sending the email.';
    }
  }
  return 'An unexpected error occurred.';
};

Rate Limiting

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { rateLimit } from './lib/rate-limit';

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/send-email')) {
    const ip = request.ip ?? '127.0.0.1';
    const { success } = await rateLimit(ip);
    
    if (!success) {
      return NextResponse.json(
        { error: 'Too many requests' },
        { status: 429 }
      );
    }
  }
  
  return NextResponse.next();
}

TypeScript Types

// types/email.ts
export interface EmailFormData {
  to: string;
  subject: string;
  html: string;
}

export interface EmailResponse {
  success: boolean;
  result?: any;
  error?: string;
}

Testing

// __tests__/api/send-email.test.ts
import { createMocks } from 'node-mocks-http';
import handler from '../../pages/api/send-email';

jest.mock('shoutboxnet', () => {
  return jest.fn().mockImplementation(() => ({
    sendEmail: jest.fn().mockResolvedValue({ success: true }),
  }));
});

describe('/api/send-email', () => {
  it('sends email successfully', async () => {
    const { req, res } = createMocks({
      method: 'POST',
      body: {
        to: '[email protected]',
        subject: 'Test',
        html: '<p>Test content</p>',
      },
    });

    await handler(req, res);

    expect(res._getStatusCode()).toBe(200);
    expect(JSON.parse(res._getData())).toEqual(
      expect.objectContaining({
        success: true,
      })
    );
  });
});