TanStack Start
Complete guide to using Unsent in TanStack Start applications with server functions and loaders
The Unsent SDK integrates seamlessly with TanStack Start, leveraging server functions and loaders. This guide covers everything from basic email sending to advanced features like campaigns, webhooks, and analytics.
Prerequisites
Node.js 18+: TanStack Start requires Node.js 18 or higher
Unsent API Key: Generate one in your Unsent dashboard
Verified Domain: Set up a domain in the Domains section
Installation
Install the SDK using your preferred package manager:
npm install @unsent/sdkpnpm add @unsent/sdkyarn add @unsent/sdkbun add @unsent/sdkConfiguration
Add your Unsent API key to .env:
UNSENT_API_KEY=un_...TanStack Start automatically loads environment variables from .env files.
Quick Start
Create a server function to send emails:
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const sendEmail = createServerFn('POST', async (payload: { email: string }) => {
const { data, error } = await unsent.emails.send({
from: 'Acme <onboarding@unsent.dev>',
to: payload.email,
subject: 'Welcome!',
html: '<h1>Hello!</h1><p>Thanks for signing up.</p>',
});
if (error) {
throw new Error(error.message);
}
return data;
});Use in your component:
import { sendEmail } from '../functions/send-email';
export default function Home() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await sendEmail({ email });
alert('Email sent successfully!');
setEmail('');
} catch (error) {
alert('Failed to send email');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Email'}
</button>
</form>
);
}Email Operations
Send with Attachments
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const sendInvoice = createServerFn('POST', async () => {
const { data, error } = await unsent.emails.send({
from: 'Billing <billing@yourdomain.com>',
to: 'customer@example.com',
subject: 'Your Invoice',
html: '<p>Please find your invoice attached.</p>',
attachments: [{
filename: 'invoice.pdf',
content: 'base64-encoded-content',
contentType: 'application/pdf'
}]
});
if (error) {
throw new Error(error.message);
}
return data;
});Schedule Emails
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const scheduleEmail = createServerFn('POST', async () => {
const scheduledTime = new Date();
scheduledTime.setHours(scheduledTime.getHours() + 24);
const { data, error } = await unsent.emails.send({
from: 'Reminders <reminders@yourdomain.com>',
to: 'user@example.com',
subject: 'Meeting Reminder',
html: '<p>Your meeting is tomorrow at 10 AM</p>',
scheduledAt: scheduledTime.toISOString()
});
if (error) {
throw new Error(error.message);
}
return data;
});Batch Sending
Send up to 100 emails in one request:
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface User {
email: string;
name: string;
}
export const sendBatchEmails = createServerFn('POST', async (users: User[]) => {
const emails = users.map(user => ({
from: 'Newsletter <news@yourdomain.com>',
to: user.email,
subject: 'Monthly Update',
html: `<h1>Hi ${user.name}!</h1><p>Here's what's new...</p>`
}));
const { data, error } = await unsent.emails.batch(emails);
if (error) {
throw new Error(error.message);
}
return data;
});List Emails
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listEmails = createServerFn('GET', async (page: number = 1) => {
const { data, count, error } = await unsent.emails.list({
page,
limit: 50
});
if (error) {
throw new Error(error.message);
}
return { data, count };
});Get Email Status
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const getEmail = createServerFn('GET', async (id: string) => {
const { data, error } = await unsent.emails.get(id);
if (error) {
throw new Error(error.message);
}
return data;
});Contact Management
Create Contact Book
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const createContactBook = createServerFn('POST', async (payload: { name: string; emoji: string }) => {
const { data, error } = await unsent.contactBooks.create(payload);
if (error) {
throw new Error(error.message);
}
return data;
});List Contact Books
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listContactBooks = createServerFn('GET', async () => {
const { data, error } = await unsent.contactBooks.list();
if (error) {
throw new Error(error.message);
}
return data;
});Manage Contacts
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface ContactPayload {
bookId: string;
contact: {
email: string;
firstName: string;
lastName: string;
subscribed: boolean;
metadata?: Record<string, any>;
};
}
export const createContact = createServerFn('POST', async (payload: ContactPayload) => {
const { data, error } = await unsent.contacts.create(
payload.bookId,
payload.contact
);
if (error) {
throw new Error(error.message);
}
return data;
});Upsert Contact
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface UpsertPayload {
bookId: string;
contactId: string;
contact: Record<string, any>;
}
export const upsertContact = createServerFn('POST', async (payload: UpsertPayload) => {
const { data, error } = await unsent.contacts.upsert(
payload.bookId,
payload.contactId,
payload.contact
);
if (error) {
throw new Error(error.message);
}
return data;
});Campaign Management
Create Campaign
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface CampaignPayload {
name: string;
subject: string;
html: string;
bookId: string;
}
export const createCampaign = createServerFn('POST', async (payload: CampaignPayload) => {
const { data, error } = await unsent.campaigns.create({
name: payload.name,
subject: payload.subject,
html: payload.html,
from: 'Newsletter <news@yourdomain.com>',
contactBookId: payload.bookId,
replyTo: 'support@yourdomain.com'
});
if (error) {
throw new Error(error.message);
}
return data;
});List Campaigns
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listCampaigns = createServerFn('GET', async () => {
const { data, error } = await unsent.campaigns.list();
if (error) {
throw new Error(error.message);
}
return data;
});Schedule Campaign
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const scheduleCampaign = createServerFn('POST', async (payload: { id: string; scheduledAt: string }) => {
const { data, error } = await unsent.campaigns.schedule(payload.id, {
scheduledAt: payload.scheduledAt,
batchSize: 1000,
batchInterval: 60
});
if (error) {
throw new Error(error.message);
}
return data;
});Domain Management
List Domains
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listDomains = createServerFn('GET', async () => {
const { data, error } = await unsent.domains.list();
if (error) {
throw new Error(error.message);
}
return data;
});Create Domain
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const createDomain = createServerFn('POST', async (payload: { name: string; region: string }) => {
const { data, error } = await unsent.domains.create(payload);
if (error) {
throw new Error(error.message);
}
return data;
});Verify Domain
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const verifyDomain = createServerFn('POST', async (id: string) => {
const { data, error } = await unsent.domains.verify(id);
if (error) {
throw new Error(error.message);
}
return data;
});Templates
Create Template
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface TemplatePayload {
name: string;
subject: string;
html: string;
content: string;
}
export const createTemplate = createServerFn('POST', async (payload: TemplatePayload) => {
const { data, error } = await unsent.templates.create(payload);
if (error) {
throw new Error(error.message);
}
return data;
});List Templates
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listTemplates = createServerFn('GET', async () => {
const { data, error } = await unsent.templates.list();
if (error) {
throw new Error(error.message);
}
return data;
});Send with Template
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
interface SendTemplatePayload {
to: string;
templateId: string;
variables: Record<string, any>;
}
export const sendTemplateEmail = createServerFn('POST', async (payload: SendTemplatePayload) => {
const { data, error } = await unsent.emails.send({
from: 'Hello <hello@yourdomain.com>',
to: payload.to,
templateId: payload.templateId,
variables: payload.variables
});
if (error) {
throw new Error(error.message);
}
return data;
});Analytics
Get Account Analytics
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const getAnalytics = createServerFn('GET', async () => {
const { data, error } = await unsent.analytics.get();
if (error) {
throw new Error(error.message);
}
return data;
});Get Time Series Data
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const getTimeSeries = createServerFn('GET', async (days: number = 30) => {
const { data, error } = await unsent.analytics.getTimeSeries({ days });
if (error) {
throw new Error(error.message);
}
return data;
});Get Reputation Score
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const getReputation = createServerFn('GET', async () => {
const { data, error } = await unsent.analytics.getReputation();
if (error) {
throw new Error(error.message);
}
return data;
});Webhooks
Manage Webhooks
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listWebhooks = createServerFn('GET', async () => {
const { data, error } = await unsent.webhooks.list();
if (error) {
throw new Error(error.message);
}
return data;
});import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const createWebhook = createServerFn('POST', async (payload: { url: string; events: string[] }) => {
const { data, error } = await unsent.webhooks.create(payload);
if (error) {
throw new Error(error.message);
}
return data;
});Suppression Management
Manage Suppressions
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listSuppressions = createServerFn('GET', async () => {
const { data, error } = await unsent.suppressions.list({
page: 1,
limit: 50
});
if (error) {
throw new Error(error.message);
}
return data;
});import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const addSuppression = createServerFn('POST', async (payload: { email: string; reason: string }) => {
const { data, error } = await unsent.suppressions.add(payload);
if (error) {
throw new Error(error.message);
}
return data;
});import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const removeSuppression = createServerFn('DELETE', async (email: string) => {
const { data, error } = await unsent.suppressions.delete(email);
if (error) {
throw new Error(error.message);
}
return data;
});API Key Management
Manage API Keys
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const listApiKeys = createServerFn('GET', async () => {
const { data, error } = await unsent.apiKeys.list();
if (error) {
throw new Error(error.message);
}
return data;
});import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const createApiKey = createServerFn('POST', async (payload: { name: string; permission: string }) => {
const { data, error } = await unsent.apiKeys.create(payload);
if (error) {
throw new Error(error.message);
}
return data;
});Using Loaders
Fetch data server-side with loaders:
import { createFileRoute } from '@tanstack/react-router';
import { listEmails } from '../functions/list-emails';
export const Route = createFileRoute('/emails')({
loader: async () => {
const result = await listEmails(1);
return result;
},
component: EmailsPage,
});
function EmailsPage() {
const { data, count } = Route.useLoaderData();
return (
<div>
<h1>Emails ({count})</h1>
<ul>
{data.map((email) => (
<li key={email.id}>
{email.subject} - {email.status}
</li>
))}
</ul>
</div>
);
}Error Handling
Comprehensive Error Handling
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const sendSafeEmail = createServerFn('POST', async (payload: { email: string }) => {
try {
const { data, error } = await unsent.emails.send({
from: 'Hello <hello@yourdomain.com>',
to: payload.email,
subject: 'Test',
html: '<p>Test</p>'
});
if (error) {
switch (error.code) {
case 'VALIDATION_ERROR':
throw new Error('Invalid email data');
case 'RATE_LIMIT_EXCEEDED':
throw new Error(`Too many requests. Retry after ${error.retryAfter}s`);
case 'DOMAIN_NOT_VERIFIED':
throw new Error('Domain not verified');
default:
throw new Error(error.message);
}
}
return data;
} catch (err) {
throw new Error('Failed to send email');
}
});Best Practices
Shared Utilities
Create shared utilities for reusability:
import { Unsent } from '@unsent/sdk';
let unsentInstance: Unsent | null = null;
export function getUnsent() {
if (!unsentInstance) {
if (!process.env.UNSENT_API_KEY) {
throw new Error('UNSENT_API_KEY is not defined');
}
unsentInstance = new Unsent(process.env.UNSENT_API_KEY);
}
return unsentInstance;
}Use in your server functions:
import { createServerFn } from '@tanstack/start';
import { getUnsent } from '../lib/unsent';
export const exampleFunction = createServerFn('POST', async () => {
const unsent = getUnsent();
// Use unsent...
});Idempotency Keys
Use idempotency keys to prevent duplicate sends:
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const sendSignupEmail = createServerFn('POST', async (payload: { userId: string; email: string }) => {
const { data, error } = await unsent.emails.send(
{
from: 'Welcome <hello@yourdomain.com>',
to: payload.email,
subject: 'Welcome!',
html: '<p>Thanks for signing up!</p>'
},
{
idempotencyKey: `signup-${payload.userId}`
}
);
if (error) {
throw new Error(error.message);
}
return data;
});Type Safety
Leverage TypeScript for type safety:
import { createServerFn } from '@tanstack/start';
import { Unsent } from '@unsent/sdk';
import type { SendEmailPayload } from '@unsent/sdk';
const unsent = new Unsent(process.env.UNSENT_API_KEY);
export const sendTypedEmail = createServerFn('POST', async (payload: SendEmailPayload) => {
const { data, error } = await unsent.emails.send(payload);
if (error) {
throw new Error(error.message);
}
return data;
});Advanced Patterns
Form with Mutations
import { createFileRoute } from '@tanstack/react-router';
import { sendEmail } from '../functions/send-email';
import { useState } from 'react';
export const Route = createFileRoute('/newsletter')({
component: NewsletterPage,
});
function NewsletterPage() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
try {
await sendEmail({ email });
setStatus('success');
setEmail('');
} catch (error) {
setStatus('error');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
/>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Subscribing...' : 'Subscribe'}
</button>
{status === 'success' && <p>Successfully subscribed!</p>}
{status === 'error' && <p>Failed to subscribe. Please try again.</p>}
</form>
);
}