Back to Blog

How to Send Emails in Bun (2026 Guide)

10 min read

Bun is a fast JavaScript runtime with built-in fetch, a native HTTP server, and npm compatibility. Email sending is one HTTP call. Install packages with bun add and they just work.

This guide covers email sending with Bun's native server and the Elysia framework.

Install

Terminal
bun add sequenzy
Terminal
bun add resend
Terminal
bun add @sendgrid/mail

Create an Email Client

src/email.ts
import Sequenzy from "sequenzy";

export const sequenzy = new Sequenzy();
// Reads SEQUENZY_API_KEY from Bun.env automatically
src/email.ts
import { Resend } from "resend";

export const resend = new Resend(Bun.env.RESEND_API_KEY);
src/email.ts
import sgMail from "@sendgrid/mail";

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

export { sgMail };

Send with Bun.serve

src/index.ts
import { sequenzy } from "./email";

Bun.serve({
port: 3000,
async fetch(req) {
  const url = new URL(req.url);

  if (req.method === "POST" && url.pathname === "/api/send-welcome") {
    const { email, name } = await req.json();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    try {
      const result = await sequenzy.transactional.send({
        to: email,
        subject: `Welcome, ${name}`,
        body: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
      });

      return Response.json({ jobId: result.jobId });
    } catch {
      return Response.json({ error: "Failed to send" }, { status: 500 });
    }
  }

  return new Response("Not found", { status: 404 });
},
});

console.log("Server running on port 3000");
src/index.ts
import { resend } from "./email";

Bun.serve({
port: 3000,
async fetch(req) {
  const url = new URL(req.url);

  if (req.method === "POST" && url.pathname === "/api/send-welcome") {
    const { email, name } = await req.json();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    const { data, error } = await resend.emails.send({
      from: "Your App <noreply@yourdomain.com>",
      to: email,
      subject: `Welcome, ${name}`,
      html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
    });

    if (error) {
      return Response.json({ error: error.message }, { status: 500 });
    }

    return Response.json({ id: data?.id });
  }

  return new Response("Not found", { status: 404 });
},
});

console.log("Server running on port 3000");
src/index.ts
import { sgMail } from "./email";

Bun.serve({
port: 3000,
async fetch(req) {
  const url = new URL(req.url);

  if (req.method === "POST" && url.pathname === "/api/send-welcome") {
    const { email, name } = await req.json();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    try {
      await sgMail.send({
        to: email,
        from: "noreply@yourdomain.com",
        subject: `Welcome, ${name}`,
        html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
      });

      return Response.json({ sent: true });
    } catch {
      return Response.json({ error: "Failed to send" }, { status: 500 });
    }
  }

  return new Response("Not found", { status: 404 });
},
});

console.log("Server running on port 3000");

Run with:

bun run src/index.ts

Send with Elysia

bun add elysia
src/index.ts
import { Elysia, t } from "elysia";
import { sequenzy } from "./email";

new Elysia()
.post("/api/send-welcome", async ({ body }) => {
  const result = await sequenzy.transactional.send({
    to: body.email,
    subject: `Welcome, ${body.name}`,
    body: `<h1>Welcome, ${body.name}</h1><p>Your account is ready.</p>`,
  });

  return { jobId: result.jobId };
}, {
  body: t.Object({
    email: t.String({ format: "email" }),
    name: t.String(),
  }),
})
.listen(3000);

console.log("Server running on port 3000");
src/index.ts
import { Elysia, t } from "elysia";
import { resend } from "./email";

new Elysia()
.post("/api/send-welcome", async ({ body }) => {
  const { data, error } = await resend.emails.send({
    from: "Your App <noreply@yourdomain.com>",
    to: body.email,
    subject: `Welcome, ${body.name}`,
    html: `<h1>Welcome, ${body.name}</h1><p>Your account is ready.</p>`,
  });

  if (error) throw new Error(error.message);
  return { id: data?.id };
}, {
  body: t.Object({
    email: t.String({ format: "email" }),
    name: t.String(),
  }),
})
.listen(3000);

console.log("Server running on port 3000");
src/index.ts
import { Elysia, t } from "elysia";
import { sgMail } from "./email";

new Elysia()
.post("/api/send-welcome", async ({ body }) => {
  await sgMail.send({
    to: body.email,
    from: "noreply@yourdomain.com",
    subject: `Welcome, ${body.name}`,
    html: `<h1>Welcome, ${body.name}</h1><p>Your account is ready.</p>`,
  });

  return { sent: true };
}, {
  body: t.Object({
    email: t.String({ format: "email" }),
    name: t.String(),
  }),
})
.listen(3000);

console.log("Server running on port 3000");

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records.

2. Use Environment Variables

# .env
SEQUENZY_API_KEY=sq_your_key

Bun loads .env automatically.

3. Build for Production

bun build --target=bun src/index.ts --outdir=dist
bun run dist/index.js

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration for SaaS.

Wrapping Up

  1. Bun.serve for lightweight HTTP servers
  2. Elysia for typed route handling with validation
  3. Auto .env loading for API key management
  4. npm compatibility for any email SDK

Pick your provider, copy the patterns, and start sending.