Search docs

Jump between documentation pages.

Browse docs

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.

Send through @sendgrid/mail
Route handlersgMailSendGrid v3 APIEvent Webhook
  1. 01requestRoute handlersgMailsgMail.send({ from, to, subject, text })API key set once via sgMail.setApiKey()
  2. 02requestsgMailSendGrid v3 APIPOST /v3/mail/sendBearer token, JSON body
  3. 03responseSendGrid v3 APIRoute handler202 AcceptedX-Message-Id header returned as { id }
  4. 04asyncSendGrid v3 APIEvent Webhookdelivered, bounce, open eventsverify the signature before trusting payloads
The plugin sets the API key once, then sgMail.send() POSTs to the v3 Mail Send API. SendGrid replies 202 with an X-Message-Id, and later posts delivery events to your Event Webhook.

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, 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.