Search docs

Jump between documentation pages.

Send email from DaloyJS with SendGrid

Twilio SendGrid is a long-standing email delivery service that combines transactional and marketing sending. This guide uses the official @sendgrid/mail SDK, which wraps the v3 Mail Send REST API.

1. Provision

  1. Create a SendGrid account, enable 2FA, then go to Settings → API Keys and generate a Restricted Access key with only Mail Send → Full Access enabled.
  2. Complete Domain Authentication (SPF/DKIM CNAMEs) for your sending domain, or use Single Sender Verification for quick tests only.

2. Install

ts
pnpm add @sendgrid/mail

3. Environment variables

ts
# .env
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxxx
SENDGRID_FROM="Acme <no-reply@acme.example.com>"

4. Plugin

ts
// src/plugins/sendgrid.ts
import sgMail from "@sendgrid/mail";
import type { App } from "@daloyjs/core";

sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

export const sendgridPlugin = {
  name: "sendgrid",
  register(app: App) {
    app.decorate("email", {
      async send({ to, subject, text, html }) {
        const [res] = await sgMail.send({
          from: process.env.SENDGRID_FROM!,
          to,
          subject,
          text,
          html,
        });
        // SendGrid returns 202 with an X-Message-Id header on success
        const id = res.headers["x-message-id"] ?? "";
        return { id: String(id) };
      },
    });
  },
};

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

ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { sendgridPlugin } from "./plugins/sendgrid";

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 10 }));
app.register(sendgridPlugin);

app.route({
  method: "POST",
  path: "/contact",
  operationId: "submitContact",
  request: {
    body: z.object({
      to: z.string().email(),
      subject: z.string().min(1).max(200),
      message: z.string().min(1).max(5000),
    }),
  },
  responses: {
    202: { description: "Queued", body: z.object({ id: z.string() }) },
  },
  handler: async ({ body, state }) => {
    const { id } = await state.email.send({
      to: body.to,
      subject: body.subject,
      text: body.message,
    });
    return { status: 202, body: { id } };
  },
});

Dynamic templates

For server-side templating, create a Dynamic Transactional Template in the SendGrid UI and pass its ID with substitution values:

ts
await sgMail.send({
  from: process.env.SENDGRID_FROM!,
  to,
  templateId: "d-1234567890abcdef1234567890abcdef",
  dynamicTemplateData: {
    firstName: "Devlin",
    cartUrl: "https://acme.example.com/cart/abc",
  },
});

Error handling

On non-2xx responses the SDK throws an error with response.body.errors describing each failure. Surface those to your client through the standard problem+json helper rather than echoing raw text.

Runtimes

The @sendgrid/mail package is Node-oriented (it uses @sendgrid/client with Node's HTTPS module). For Cloudflare Workers or Vercel Edge, call the v3 REST API directly with fetch against https://api.sendgrid.com/v3/mail/send using the same JSON body and a Bearer token.

See also Resend, Postmark, and the email integrations overview.