Angular: SSR e AnalogJS
Invia email da un'applicazione Angular usando @tratto/email sul server.
Non esiste un SDK Angular. Le applicazioni Angular inviano email dal server,
usando @tratto/email nel processo Node che renderizza o
serve l'applicazione.
Una chiave API dà accesso completo al workspace: inviare dai tuoi domini verificati, leggere la lista contatti, creare nuove chiavi API, rimuovere membri del team, eliminare il workspace. Una chiave spedita al browser è pubblica: si legge nel bundle JavaScript, nei DevTools e nel tab di rete. Nessuna offuscazione cambia questo, ed è il motivo per cui nessun provider email pubblica un SDK per browser.
Il tranello specifico di Angular
React ha un confine di compilazione tra codice server e codice client. Angular no. Tutto ciò che scrivi in un componente, in un servizio o in un resolver finisce compilato nel bundle del browser, anche in un'applicazione SSR. Il server side rendering cambia dove il codice viene eseguito la prima volta: non impedisce al codice di raggiungere il client.
Quindi "usiamo SSR" non è di per sé una protezione. Quello che conta è dove sta la chiave, e se il codice che la legge può mai essere eseguito in un browser.
// ❌ MAI: app.config.ts fa parte del bundle browser
export const appConfig: ApplicationConfig = {
providers: [
{ provide: TRATTO_KEY, useValue: 'tratto_live_...' }, // spedita a ogni visitatore
],
};L'applicazione continua a funzionare, ed è questo che rende l'errore facile da non notare.
Pattern 1: Chiama Tratto dalle tue route server (consigliato)
Il design più sicuro tiene Tratto completamente fuori dall'applicazione Angular. Il tuo server espone un endpoint, il browser chiama quell'endpoint e il server parla con Tratto.
// server.ts
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Tratto } from '@tratto/email';
const browserDistFolder = resolve(
dirname(fileURLToPath(import.meta.url)),
'../browser',
);
const app = express();
const angularApp = new AngularNodeAppEngine();
const tratto = new Tratto(process.env['TRATTO_API_KEY']!);
app.use(express.json());
// Endpoint tuo: il browser non vede mai la chiave Tratto.
app.post('/api/contact', async (req, res, next) => {
try {
// Valida prima di inviare. Mai interpolare input utente in `from`.
const { email, message } = req.body ?? {};
if (typeof email !== 'string' || typeof message !== 'string') {
return res.status(400).json({ error: 'Invalid payload' });
}
const result = await tratto.emails.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
replyTo: email,
subject: 'Nuovo messaggio dal form di contatto',
text: message,
});
// Restituisci solo ciò che serve al client.
res.json({ id: result.id });
} catch (error) {
next(error);
}
});
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
app.use((req, res, next) => {
angularApp
.handle(req)
.then((response) =>
response ? writeResponseToNodeResponse(response, res) : next(),
)
.catch(next);
});
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4000;
app.listen(port);
}
export const reqHandler = createNodeRequestHandler(app);server.ts viene impacchettato nell'output server e non raggiunge mai il browser.
Pattern 2: Recuperare dati durante il render server
Se ti servono dati Tratto mentre la pagina viene renderizzata, dichiara un tuo injection token e forniscilo solo nella configurazione server.
Il file del token si può importare ovunque, perché dichiara un token e usa
import type per il client: dall'SDK non viene emesso nulla.
// app/tratto.token.ts
import { InjectionToken } from '@angular/core';
import type { Tratto } from '@tratto/email';
export const TRATTO = new InjectionToken<Tratto>('TRATTO');Il provider esiste solo nel bundle server:
// app.config.server.ts: solo bundle server
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/ssr';
import { Tratto } from '@tratto/email';
import { appConfig } from './app.config';
import { TRATTO } from './tratto.token';
const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
{
provide: TRATTO,
useFactory: () => new Tratto(process.env['TRATTO_API_KEY']!),
},
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);Nel browser il provider semplicemente non c'è, quindi optional: true restituisce
null e la chiamata viene saltata:
import { Component, inject, signal } from '@angular/core';
import { TRATTO } from './tratto.token';
@Component({
selector: 'app-newsletter-stats',
template: `<p>{{ subscribers() }} iscritti</p>`,
})
export class NewsletterStatsComponent {
readonly subscribers = signal(0);
constructor() {
// null nel browser: il provider è solo lato server.
const tratto = inject(TRATTO, { optional: true });
if (!tratto) return;
tratto.analytics
.getSummary({ period: '30d' })
.then((summary) => this.subscribers.set(summary.subscribers));
}
}Trasferisci il risultato con lo state transfer di Angular, così il valore sopravvive all'hydration senza una seconda richiesta.
Preferisci il Pattern 1 ogni volta che il dato non deve stare nell'HTML iniziale. Tiene l'SDK completamente fuori dal grafo delle dipendenze dell'applicazione: una cosa in meno che può rompersi durante un refactor.
Tipizzare i dati nel browser
@tratto/email esporta tutti i tipi di richiesta e risposta, e import type
viene cancellato in fase di compilazione: nel bundle non arriva nulla.
import type { EmailEvent } from '@tratto/email';
export interface DeliveryRow {
event: EmailEvent;
}Scrivi import type, non import. Con verbatimModuleSyntax TypeScript emette
l'import così com'è, trascinando l'intero SDK nel bundle browser. Attiva
@typescript-eslint/consistent-type-imports per farlo verificare dal linter.
È 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.
AnalogJS e Nitro
AnalogJS è il meta-framework Angular costruito su Nitro. Le sue route API server sono l'equivalente del Pattern 1:
// src/server/routes/api/contact.post.ts
import { defineEventHandler, readBody, createError } from 'h3';
import { Tratto } from '@tratto/email';
const tratto = new Tratto(process.env['TRATTO_API_KEY']!);
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (typeof body?.email !== 'string') {
throw createError({ statusCode: 400, statusMessage: 'Invalid payload' });
}
const result = await tratto.emails.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
replyTo: body.email,
subject: 'Nuovo messaggio dal form di contatto',
text: String(body.message ?? ''),
});
return { id: result.id };
});I file sotto src/server/ vengono impacchettati da Nitro nell'output server e non
raggiungono mai il browser.
Fornire la chiave
Leggi la chiave dall'ambiente. Non committarla mai e non metterla in
environment.ts: i file environment di Angular fanno parte del bundle browser.
# .env (in gitignore)
TRATTO_API_KEY=tratto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxIn sviluppo usa chiavi tratto_test_..., così un errore non può inviare email
reali.
Gestione degli errori
@tratto/email solleva TrattoError con il codice di errore dell'API. Logga il
dettaglio lato server e restituisci qualcosa di generico:
import { TrattoError } from '@tratto/email';
try {
await tratto.emails.send({ /* … */ });
} catch (error) {
if (error instanceof TrattoError) {
console.error(error.code, error.statusCode);
}
res.status(500).json({ error: 'Invio non riuscito' });
}I payload di errore di Tratto possono contenere indirizzi dei destinatari e altri dettagli che non devono uscire dal tuo server. Vedi Codici di Errore.
Checklist
- La chiave è letta da
process.env, mai daenvironment.ts - Nessun provider che porta la chiave esiste in
app.config.ts - I componenti usano
inject(TRATTO, { optional: true })e gestiscononull - I tuoi endpoint 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" dist/browsernon trova nulla dopo una build di produzione
Metti quest'ultimo controllo in CI.
Correlati: SDK Node.js · Autenticazione · React
Modifica questa pagina su GitHub
Ultimo aggiornamento