Send email from DaloyJS with AWS SES
Amazon Simple Email Service (SES) is AWS's pay-as-you-go transactional and bulk email service. This guide uses SESv2 — the current API — through the AWS SDK for JavaScript v3. Best fit when you already run on AWS (Lambda, ECS, Fargate, EC2) or need very low per-message cost.
1. Provision
- In the AWS console, open Amazon SES → Verified identities and verify either an email address (for development) or your sending domain (for production). Add the SPF, DKIM, and DMARC records SES shows you.
- New accounts start in the SES sandbox: you can only send to verified addresses. Request production access from Account dashboard before launching.
- Create an IAM principal that can call
ses:SendEmail. Prefer an execution role attached to your Lambda or container — avoid long-lived access keys.
2. Install
pnpm add @aws-sdk/client-sesv2The v3 SDK is modular — install only the SESv2 client. It works on Node 18+ and is compatible with the Lambda adapter.
3. Environment variables
# .env
AWS_REGION=us-east-1
SES_FROM_ADDRESS="Acme <no-reply@acme.example.com>"
# Local development only — in AWS, prefer the execution role
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...4. Plugin
The SDK reads credentials from the standard AWS provider chain (env vars, shared config file, IMDS, IRSA on EKS, Lambda role), so the client itself takes no secrets.
// src/plugins/ses.ts
import {
SESv2Client,
SendEmailCommand,
type SendEmailCommandInput,
} from "@aws-sdk/client-sesv2";
import type { App } from "@daloyjs/core";
const client = new SESv2Client({ region: process.env.AWS_REGION });
export const sesPlugin = {
name: "ses",
register(app: App) {
app.decorate("email", {
async send({ to, subject, text, html }) {
const input: SendEmailCommandInput = {
FromEmailAddress: process.env.SES_FROM_ADDRESS!,
Destination: { ToAddresses: [to] },
Content: {
Simple: {
Subject: { Data: subject, Charset: "UTF-8" },
Body: {
...(text ? { Text: { Data: text, Charset: "UTF-8" } } : {}),
...(html ? { Html: { Data: html, Charset: "UTF-8" } } : {}),
},
},
},
};
const out = await client.send(new SendEmailCommand(input));
return { id: out.MessageId ?? "" };
},
});
app.onClose(() => client.destroy());
},
};
declare module "@daloyjs/core" {
interface AppState {
email: {
send(msg: {
to: string;
subject: string;
text?: string;
html?: string;
}): Promise<{ id: string }>;
};
}
}5. Use it in a route
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { sesPlugin } from "./plugins/ses";
const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 10 }));
app.register(sesPlugin);
app.route({
method: "POST",
path: "/notify",
operationId: "sendWelcome",
request: {
body: z.object({
to: z.string().email(),
name: z.string().min(1).max(80),
}),
},
responses: {
202: { description: "Queued", body: z.object({ id: z.string() }) },
},
handler: async ({ body, state }) => {
const { id } = await state.email.send({
to: body.to,
subject: `Welcome, ${body.name}!`,
text: `Hi ${body.name}, thanks for signing up.`,
html: `<p>Hi ${body.name}, thanks for signing up.</p>`,
});
return { status: 202, body: { id } };
},
});Templates & attachments
SESv2's SendEmailCommand accepts three content variants:
Content.Simple— subject + text/HTML body (used above). Also supports anAttachmentsarray with base64RawContent,FileName, andContentType.Content.Raw.Data— a fully MIME-encoded message (use mailcomposer ornodemailer's composer if you need rich attachments).Content.Template— render an SES template byTemplateNamewith a JSONTemplateDatapayload. Create templates ahead of time withCreateEmailTemplateCommand.
Runtimes
- Node / Bun / Deno / AWS Lambda — works out of the box. On Lambda, omit access keys and let the execution role supply credentials.
- Cloudflare Workers / Vercel Edge — the SDK can run there but uses a Web Crypto signer; pin
@aws-sdk/client-sesv2≥ 3.700 and passcredentialsexplicitly (the default provider chain expects Node APIs).
Observability
SES publishes delivery, bounce, and complaint events to SNS, EventBridge, or Kinesis Firehose via a configuration set. Add ConfigurationSetName to SendEmailCommandInput to opt in.
See also Resend, SendGrid, and the email integrations overview.