React: Server Component e Server Action
Invia email da React usando @tratto/email sul server. Non esiste un SDK browser.
Non esiste un pacchetto @tratto/react, e non esisterà. Le applicazioni React
inviano email dal server, usando @tratto/email dentro un
Server Component, una Server Action o un Route Handler.
Non è un limite di Tratto. Nessun provider di email transazionali pubblica un SDK per browser, perché una chiave API dà accesso completo all'account: inviare dai tuoi domini verificati, leggere i contatti, creare nuove chiavi, eliminare il workspace. Una chiave nel bundle browser è una chiave che hai pubblicato.
React si presta particolarmente bene a questo approccio. I Server Component offrono un vero confine di compilazione (il codice in un file server non raggiunge mai il bundle client), quindi il pattern corretto è anche quello naturale.
Installazione
npm install @tratto/emailTieni la chiave nell'ambiente, senza il prefisso NEXT_PUBLIC_. È esattamente
quel prefisso a inlinare un valore dentro il bundle browser.
# .env.local (in gitignore)
TRATTO_API_KEY=tratto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxIn sviluppo usa tratto_test_..., così un errore non può inviare email reali.
Proteggi il modulo
Installa il pacchetto server-only e importalo in cima a ogni file che tocca
l'SDK. Se quel modulo finisce per errore dentro un Client Component, la build
fallisce invece di spedire la tua chiave.
npm install server-only// lib/tratto.ts
import 'server-only';
import { Tratto } from '@tratto/email';
export const tratto = new Tratto(process.env.TRATTO_API_KEY!);Questo singolo import è la differenza tra un errore intercettato in fase di build e una chiave finita in produzione. Aggiungilo prima di scrivere la prima chiamata.
Server Action
Un form che invia a una Server Action non espone mai la chiave: il corpo della funzione resta sul server e il browser trasmette solo i dati del form.
// app/contact/actions.ts
'use server';
import { tratto } from '@/lib/tratto';
export async function sendContactMessage(formData: FormData) {
const email = formData.get('email');
const message = formData.get('message');
// Valida sul server. Non fidarti mai dei valori inviati.
if (typeof email !== 'string' || typeof message !== 'string') {
return { ok: false, error: 'Invio non valido' };
}
try {
await tratto.emails.send({
// `from` è un indirizzo verificato fisso, mai costruito da input utente.
from: 'Acme <[email protected]>',
to: '[email protected]',
replyTo: email,
subject: 'Nuovo messaggio dal form di contatto',
text: message,
});
return { ok: true };
} catch (error) {
// Logga il dettaglio, restituisci qualcosa di generico.
console.error('tratto.emails.send fallita', error);
return { ok: false, error: 'Non è stato possibile inviare il messaggio' };
}
}// app/contact/page.tsx
import { sendContactMessage } from './actions';
export default function ContactPage() {
return (
<form action={sendContactMessage}>
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit">Invia</button>
</form>
);
}Server Component
Leggi i dati durante il render. Il componente gira sul server, quindi la chiamata all'SDK non raggiunge mai il client.
// app/dashboard/page.tsx
import { tratto } from '@/lib/tratto';
export default async function DashboardPage() {
const summary = await tratto.analytics.getSummary({ period: '30d' });
return (
<dl>
<dt>Consegnate</dt>
<dd>{summary.delivered}</dd>
<dt>Aperte</dt>
<dd>{summary.opened}</dd>
</dl>
);
}Passa ai Client Component solo i valori che servono. Tutto ciò che attraversa quel
confine viene serializzato nel payload HTML ed è leggibile dal visitatore: passa
summary.delivered, non l'intera risposta dell'API.
Tipizzare i dati nei Client Component
Non esiste un pacchetto di tipi separato, e non serve. @tratto/email esporta
tutti i tipi di richiesta e risposta, e import type viene cancellato in fase di
compilazione: nel bundle non finisce nulla.
// components/status-badge.tsx
'use client';
import type { EmailEvent } from '@tratto/email';
export function StatusBadge({ event }: { event: EmailEvent }) {
return <span>{event.type}</span>;
}Scrivi import type, non import. Con verbatimModuleSyntax TypeScript emette
l'import così com'è, trascinando l'intero SDK (e la gestione della chiave API)
dentro il bundle client. Attiva
@typescript-eslint/consistent-type-imports per farlo verificare dal linter.
Usa questi tipi quando il tuo server inoltra le strutture di Tratto, cioè
soprattutto per gli eventi webhook. Per tutto il resto è preferibile restituire
dal server un oggetto ristretto e tipizzare quello: il browser non ha motivo di
ricevere un EmailDetail completo per mostrare una stringa di stato.
Route Handler
Usa un Route Handler quando a chiamarti è qualcosa di diverso da un form: un
ricevitore di webhook, una fetch lato client o un servizio esterno.
// app/api/subscribe/route.ts
import { NextResponse } from 'next/server';
import { tratto } from '@/lib/tratto';
export async function POST(request: Request) {
const body = await request.json().catch(() => null);
if (!body || typeof body.email !== 'string') {
return NextResponse.json({ error: 'Payload non valido' }, { status: 400 });
}
try {
const contact = await tratto.contacts.create({
email: body.email,
audienceId: 'aud_abc123',
});
return NextResponse.json({ id: contact.id });
} catch (error) {
console.error('tratto.contacts.create fallita', error);
return NextResponse.json(
{ error: 'Iscrizione non riuscita' },
{ status: 500 },
);
}
}Applica un rate limit a ogni endpoint raggiungibile da internet. Senza, un form che invia email diventa il modo con cui chiunque può consumare la tua quota di invio e danneggiare la reputazione del tuo dominio. Vedi Rate Limit.
Cosa non fare
// ❌ MAI: 'use client' porta questo codice nel bundle browser
'use client';
import { Tratto } from '@tratto/email';
// Pubblicata a ogni visitatore.
const tratto = new Tratto(process.env.NEXT_PUBLIC_TRATTO_API_KEY!);Sono due errori distinti, e ciascuno basterebbe da solo: l'SDK viene istanziato in
un Client Component, e la chiave usa NEXT_PUBLIC_, che la inlina come stringa
letterale in fase di build.
L'import di server-only mostrato sopra trasforma il primo errore in un errore di
build. Per il secondo, fai grep sull'output della build.
Comporre le email con React
C'è un punto in cui React è davvero utile per le email: scrivere i template. Librerie come React Email permettono di costruire un messaggio come componenti React e renderizzarlo in HTML sul server, per poi passare il risultato a Tratto:
// app/emails/welcome.tsx
export function WelcomeEmail({ name }: { name: string }) {
return (
<div>
<h1>Benvenuto, {name}</h1>
<p>Grazie per esserti registrato.</p>
</div>
);
}import { render } from '@react-email/render';
import { WelcomeEmail } from '@/app/emails/welcome';
import { tratto } from '@/lib/tratto';
const html = await render(<WelcomeEmail name="Alice" />);
await tratto.emails.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
subject: 'Benvenuto',
html,
});Questo è rendering, non invio: avviene sul server e non comporta nessuna chiave API nel browser. I Template di Tratto coprono la stessa esigenza se preferisci gestire i contenuti fuori dal codice.
Checklist
-
import 'server-only'in cima al modulo che crea il client - La chiave è
TRATTO_API_KEY, maiNEXT_PUBLIC_TRATTO_API_KEY - Nessun file
'use client'importa@tratto/email - Gli endpoint pubblici validano l'input e hanno un rate limit
-
fromè un indirizzo fisso e verificato, mai costruito da input utente - Gli errori sono loggati sul server e restituiti in forma generica
-
grep -r "tratto_live" .next/staticnon trova nulla dopo una build di produzione
Metti quest'ultimo controllo in CI.
Correlati: SDK Node.js · Autenticazione · Angular
Modifica questa pagina su GitHub
Ultimo aggiornamento