Search docs

Jump between documentation pages.

Send email from DaloyJS with Resend

Resend is a developer-first email API with first-class TypeScript types, edge-runtime support, and tight integration with React Email templates. It's an excellent default for new DaloyJS projects.

1. Provision

  1. Sign up at resend.com and add a sending domain under Domains. Add the SPF, DKIM, and DMARC DNS records Resend lists, then click Verify.
  2. Create an API key under API Keys. Use a “Sending access” key scoped to that domain.

2. Install

ts
pnpm add resend

3. Environment variables

ts
# .env
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxx
RESEND_FROM="Acme <no-reply@acme.example.com>"

4. Plugin

ts
// src/plugins/resend.ts
import { Resend } from "resend";
import type { App } from "@daloyjs/core";

const resend = new Resend(process.env.RESEND_API_KEY);

export const resendPlugin = {
  name: "resend",
  register(app: App) {
    app.decorate("email", {
      async send({ to, subject, text, html }) {
        const { data, error } = await resend.emails.send({
          from: process.env.RESEND_FROM!,
          to,
          subject,
          text,
          html,
        });
        if (error) throw new Error(error.message);
        return { id: data?.id ?? "" };
      },
    });
  },
};

declare module "@daloyjs/core" {
  interface AppState {
    email: {
      send(msg: {
        to: string;
        subject: string;
        text?: string;
        html?: string;
      }): Promise<{ id: string }>;
    };
  }
}

Resend SDK methods return { data, error } rather than throwing — handle the error branch and surface it through the DaloyJS error helpers.

5. Use it in a route

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

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

app.route({
  method: "POST",
  path: "/magic-link",
  operationId: "sendMagicLink",
  request: {
    body: z.object({ email: z.string().email() }),
  },
  responses: {
    202: { description: "Sent", body: z.object({ id: z.string() }) },
  },
  handler: async ({ body, state }) => {
    const link = await issueMagicLink(body.email); // your own logic
    const { id } = await state.email.send({
      to: body.email,
      subject: "Your sign-in link",
      text: `Sign in: ${link}`,
      html: `<p>Sign in: <a href="${link}">${link}</a></p>`,
    });
    return { status: 202, body: { id } };
  },
});

async function issueMagicLink(_email: string) {
  return "https://acme.example.com/auth/callback?token=...";
}

React Email templates

Resend reads the react field and renders it to HTML for you, so you can ship type-safe email templates as React components:

ts
pnpm add @react-email/components react react-dom
ts
// emails/welcome.tsx
import { Html, Button, Heading, Text } from "@react-email/components";

export default function Welcome({ name }: { name: string }) {
  return (
    <Html>
      <Heading>Welcome, {name}!</Heading>
      <Text>Thanks for joining Acme.</Text>
      <Button href="https://acme.example.com/start">Get started</Button>
    </Html>
  );
}

// in the handler
import Welcome from "../../emails/welcome";

await resend.emails.send({
  from: process.env.RESEND_FROM!,
  to,
  subject: "Welcome to Acme",
  react: Welcome({ name: "Devlin" }),
});

Batch sending

Use resend.batch.send([...]) to enqueue up to 100 messages in a single API call — handy for fan-out notifications without queueing infrastructure.

Runtimes

The resend SDK uses the standard fetch API, so it runs on Node 18+, Bun, Deno, AWS Lambda, Vercel (Serverless and Edge), and Cloudflare Workers without adapters. Pair it with the edge adapters shipped by DaloyJS.

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