How to Send Emails in Deno (2026 Guide)

Deno has fetch built in. No extra packages needed to make HTTP requests. Email sending is one API call. Deno's permission model adds a security layer on top: your code can only access the network and environment variables if you explicitly allow it with --allow-net and --allow-env.
Most Deno email tutorials show a single fetch call and stop. This guide covers the full picture: email client abstraction, Fresh framework integration, React Email templates, Stripe webhooks, error handling, and Deno Deploy. If you're using Hono on Deno, see our dedicated Hono email guide. For other runtimes, check out Bun or Node.js.
Pick a Provider
Every code example below lets you switch between three providers.
- Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences, subscriber management, all from one SDK. Native Stripe integration and built-in retries.
- Resend is developer-friendly. Clean API, solid deliverability. They have one-off broadcast campaigns but no automations or sequences.
- SendGrid is the enterprise option. Feature-rich, high volume. Bigger API surface.
Create an Email Client
Deno's built-in fetch is all you need. No npm packages required. Create a typed client:
const SEQUENZY_API_KEY = Deno.env.get("SEQUENZY_API_KEY")!;
const BASE_URL = "https://api.sequenzy.com/v1";
interface SendResult {
id: string;
success: boolean;
}
export async function sendEmail(to: string, subject: string, body: string): Promise<SendResult> {
const response = await fetch(`${BASE_URL}/transactional/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ to, subject, body }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`Email failed (${response.status}): ${error.message || response.statusText}`);
}
return response.json();
}const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY")!;
interface SendResult {
id: string;
}
export async function sendEmail(to: string, subject: string, html: string): Promise<SendResult> {
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "YourApp <noreply@yourdomain.com>",
to,
subject,
html,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`Email failed (${response.status}): ${error.message || response.statusText}`);
}
return response.json();
}const SENDGRID_API_KEY = Deno.env.get("SENDGRID_API_KEY")!;
export async function sendEmail(to: string, subject: string, html: string): Promise<void> {
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: to }] }],
from: { email: "noreply@yourdomain.com" },
subject,
content: [{ type: "text/html", value: html }],
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Email failed (${response.status}): ${error}`);
}
}No npm install needed. Deno's fetch is a first-class citizen that works exactly like the browser fetch API.
Send with Deno.serve
Deno.serve is the simplest way to create an HTTP server. Here's a complete API with routing:
import { sendEmail } from "./lib/email.ts";
Deno.serve({ port: 3000 }, async (req) => {
const url = new URL(req.url);
// Contact form endpoint
if (req.method === "POST" && url.pathname === "/api/contact") {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch (error) {
console.error("Failed to send:", error);
return Response.json({ error: "Failed to send message" }, { status: 500 });
}
}
// Welcome email endpoint
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 sendEmail(
email,
`Welcome, ${name}!`,
`<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
);
return Response.json(result);
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
}
return new Response("Not Found", { status: 404 });
});import { sendEmail } from "./lib/email.ts";
Deno.serve({ port: 3000 }, async (req) => {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/api/contact") {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch (error) {
console.error("Failed to send:", error);
return Response.json({ error: "Failed to send message" }, { status: 500 });
}
}
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 sendEmail(
email,
`Welcome, ${name}!`,
`<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
);
return Response.json(result);
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
}
return new Response("Not Found", { status: 404 });
});import { sendEmail } from "./lib/email.ts";
Deno.serve({ port: 3000 }, async (req) => {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/api/contact") {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch (error) {
console.error("Failed to send:", error);
return Response.json({ error: "Failed to send message" }, { status: 500 });
}
}
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 sendEmail(
email,
`Welcome, ${name}!`,
`<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
);
return Response.json({ success: true });
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
}
return new Response("Not Found", { status: 404 });
});Run with:
deno run --allow-net --allow-env main.tsThe --allow-net flag enables network access and --allow-env allows reading environment variables. You can restrict these to specific domains and variables for tighter security:
deno run --allow-net=api.sequenzy.com,0.0.0.0:3000 --allow-env=SEQUENZY_API_KEY main.tsFresh Framework
Fresh is Deno's full-stack framework. It has file-based routing, islands for interactivity, and server-side rendering.
import type { Handlers } from "$fresh/server.ts";
import { sendEmail } from "../../lib/email.ts";
export const handler: Handlers = {
async POST(req) {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
},
};import type { Handlers } from "$fresh/server.ts";
import { sendEmail } from "../../lib/email.ts";
export const handler: Handlers = {
async POST(req) {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
},
};import type { Handlers } from "$fresh/server.ts";
import { sendEmail } from "../../lib/email.ts";
export const handler: Handlers = {
async POST(req) {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return Response.json({ error: "All fields are required" }, { status: 400 });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`
<h2>New Contact Form</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
);
return Response.json({ success: true });
} catch {
return Response.json({ error: "Failed to send" }, { status: 500 });
}
},
};Fresh can also handle form submissions server-side. The handler runs on the server, and the page renders with the result:
// routes/contact.tsx
import type { Handlers, PageProps } from "$fresh/server.ts";
import { sendEmail } from "../lib/email.ts";
interface Data {
success?: boolean;
error?: string;
}
export const handler: Handlers<Data> = {
async POST(req, ctx) {
const form = await req.formData();
const name = form.get("name") as string;
const email = form.get("email") as string;
const message = form.get("message") as string;
if (!name || !email || !message) {
return ctx.render({ error: "All fields are required" });
}
try {
await sendEmail(
"you@yourcompany.com",
`Contact from ${name}`,
`<p><strong>${name}</strong> (${email}): ${message}</p>`,
);
return ctx.render({ success: true });
} catch {
return ctx.render({ error: "Failed to send" });
}
},
};
export default function ContactPage({ data }: PageProps<Data>) {
return (
<div>
<h1>Contact Us</h1>
{data?.success ? (
<p style="color: green">Message sent!</p>
) : (
<form method="POST">
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="Your email" required />
<textarea name="message" placeholder="Your message" required />
<button type="submit">Send</button>
{data?.error && <p style="color: red">{data.error}</p>}
</form>
)}
</div>
);
}React Email Templates
React Email works in Deno since Deno supports npm packages. Install it:
deno add npm:@react-email/components npm:react npm:react-dom// emails/welcome.tsx
import { Html, Body, Container, Text, Button, Heading, Hr } from "npm:@react-email/components";
interface WelcomeEmailProps {
name: string;
loginUrl: string;
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Body style={{ backgroundColor: "#f6f9fc", fontFamily: "sans-serif" }}>
<Container style={{
backgroundColor: "#ffffff",
padding: "40px",
borderRadius: "8px",
margin: "40px auto",
maxWidth: "560px",
}}>
<Heading as="h1" style={{ fontSize: "24px", color: "#1a1a1a" }}>
Welcome, {name}!
</Heading>
<Text style={{ fontSize: "16px", color: "#4a4a4a", lineHeight: "26px" }}>
Your account is ready. Set up your first project, invite your team, and connect your integrations.
</Text>
<Button
href={loginUrl}
style={{
backgroundColor: "#5046e5",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
textDecoration: "none",
display: "inline-block",
marginTop: "16px",
}}
>
Go to Dashboard
</Button>
<Hr style={{ borderColor: "#e6ebf1", margin: "32px 0" }} />
<Text style={{ color: "#8898aa", fontSize: "12px" }}>
YourApp Inc. · 123 Main St · San Francisco, CA
</Text>
</Container>
</Body>
</Html>
);
}Render and send:
import { render } from "npm:@react-email/components";
import { sendEmail } from "./email.ts";
import { WelcomeEmail } from "../emails/welcome.tsx";
export async function sendWelcomeEmail(to: string, name: string) {
const html = await render(<WelcomeEmail name={name} loginUrl="https://app.yoursite.com/login" />);
return sendEmail(to, `Welcome to YourApp, ${name}!`, html);
}import { render } from "npm:@react-email/components";
import { sendEmail } from "./email.ts";
import { WelcomeEmail } from "../emails/welcome.tsx";
export async function sendWelcomeEmail(to: string, name: string) {
const html = await render(<WelcomeEmail name={name} loginUrl="https://app.yoursite.com/login" />);
return sendEmail(to, `Welcome to YourApp, ${name}!`, html);
}import { render } from "npm:@react-email/components";
import { sendEmail } from "./email.ts";
import { WelcomeEmail } from "../emails/welcome.tsx";
export async function sendWelcomeEmail(to: string, name: string) {
const html = await render(<WelcomeEmail name={name} loginUrl="https://app.yoursite.com/login" />);
return sendEmail(to, `Welcome to YourApp, ${name}!`, html);
}Common SaaS Patterns
Password Reset
import { sendEmail } from "../../lib/email.ts";
import { render } from "npm:@react-email/components";
import { Html, Body, Container, Text, Button } from "npm:@react-email/components";
function ResetEmail({ url }: { url: string }) {
return (
<Html>
<Body style={{ fontFamily: "sans-serif", backgroundColor: "#f6f9fc" }}>
<Container style={{ backgroundColor: "#fff", padding: "40px", borderRadius: "8px", margin: "40px auto" }}>
<Text style={{ fontSize: "20px", fontWeight: "bold" }}>Reset your password</Text>
<Text style={{ color: "#4a4a4a" }}>Click below to choose a new password. Expires in 1 hour.</Text>
<Button href={url} style={{ backgroundColor: "#5046e5", color: "#fff", padding: "12px 24px", borderRadius: "6px" }}>
Reset Password
</Button>
</Container>
</Body>
</Html>
);
}
export const handler: Handlers = {
async POST(req) {
const { email } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
const user = await db.user.findByEmail(email);
if (!user) return Response.json({ success: true });
const token = crypto.randomUUID();
await db.resetToken.create({ userId: user.id, token, expires: new Date(Date.now() + 3600000) });
const html = await render(<ResetEmail url={`${Deno.env.get("APP_URL")}/reset-password?token=${token}`} />);
await sendEmail(email, "Reset your password", html);
return Response.json({ success: true });
},
};import { sendEmail } from "../../lib/email.ts";
import { render } from "npm:@react-email/components";
import { Html, Body, Container, Text, Button } from "npm:@react-email/components";
function ResetEmail({ url }: { url: string }) {
return (
<Html>
<Body style={{ fontFamily: "sans-serif", backgroundColor: "#f6f9fc" }}>
<Container style={{ backgroundColor: "#fff", padding: "40px", borderRadius: "8px", margin: "40px auto" }}>
<Text style={{ fontSize: "20px", fontWeight: "bold" }}>Reset your password</Text>
<Text style={{ color: "#4a4a4a" }}>Click below to choose a new password. Expires in 1 hour.</Text>
<Button href={url} style={{ backgroundColor: "#5046e5", color: "#fff", padding: "12px 24px", borderRadius: "6px" }}>
Reset Password
</Button>
</Container>
</Body>
</Html>
);
}
export const handler: Handlers = {
async POST(req) {
const { email } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
const user = await db.user.findByEmail(email);
if (!user) return Response.json({ success: true });
const token = crypto.randomUUID();
await db.resetToken.create({ userId: user.id, token, expires: new Date(Date.now() + 3600000) });
const html = await render(<ResetEmail url={`${Deno.env.get("APP_URL")}/reset-password?token=${token}`} />);
await sendEmail(email, "Reset your password", html);
return Response.json({ success: true });
},
};import { sendEmail } from "../../lib/email.ts";
import { render } from "npm:@react-email/components";
import { Html, Body, Container, Text, Button } from "npm:@react-email/components";
function ResetEmail({ url }: { url: string }) {
return (
<Html>
<Body style={{ fontFamily: "sans-serif", backgroundColor: "#f6f9fc" }}>
<Container style={{ backgroundColor: "#fff", padding: "40px", borderRadius: "8px", margin: "40px auto" }}>
<Text style={{ fontSize: "20px", fontWeight: "bold" }}>Reset your password</Text>
<Text style={{ color: "#4a4a4a" }}>Click below to choose a new password. Expires in 1 hour.</Text>
<Button href={url} style={{ backgroundColor: "#5046e5", color: "#fff", padding: "12px 24px", borderRadius: "6px" }}>
Reset Password
</Button>
</Container>
</Body>
</Html>
);
}
export const handler: Handlers = {
async POST(req) {
const { email } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
const user = await db.user.findByEmail(email);
if (!user) return Response.json({ success: true });
const token = crypto.randomUUID();
await db.resetToken.create({ userId: user.id, token, expires: new Date(Date.now() + 3600000) });
const html = await render(<ResetEmail url={`${Deno.env.get("APP_URL")}/reset-password?token=${token}`} />);
await sendEmail(email, "Reset your password", html);
return Response.json({ success: true });
},
};Note: Deno has crypto.randomUUID() built in (web-standard), no import needed.
Stripe Webhook
Deno uses web-standard Request, so request.text() gives you the raw body:
import Stripe from "npm:stripe";
import { sendEmail } from "../../lib/email.ts";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
export const handler: Handlers = {
async POST(req) {
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, signature, Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
);
} catch {
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const email = session.customer_details?.email;
if (email) {
await sendEmail(
email,
"Payment received — thank you!",
`<h2>Payment Receipt</h2><p>Thanks for your purchase of $${(session.amount_total! / 100).toFixed(2)}.</p>`,
);
}
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.customer_email) {
await sendEmail(
invoice.customer_email,
"Payment failed — action required",
`<h2>Your payment didn't go through</h2><p>Please update your payment method.</p>`,
);
}
break;
}
}
return Response.json({ received: true });
},
};import Stripe from "npm:stripe";
import { sendEmail } from "../../lib/email.ts";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
export const handler: Handlers = {
async POST(req) {
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, signature, Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
);
} catch {
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const email = session.customer_details?.email;
if (email) {
await sendEmail(
email,
"Payment received — thank you!",
`<h2>Payment Receipt</h2><p>Thanks for your purchase of $${(session.amount_total! / 100).toFixed(2)}.</p>`,
);
}
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.customer_email) {
await sendEmail(
invoice.customer_email,
"Payment failed — action required",
`<h2>Your payment didn't go through</h2><p>Please update your payment method.</p>`,
);
}
break;
}
}
return Response.json({ received: true });
},
};import Stripe from "npm:stripe";
import { sendEmail } from "../../lib/email.ts";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
export const handler: Handlers = {
async POST(req) {
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body, signature, Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
);
} catch {
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const email = session.customer_details?.email;
if (email) {
await sendEmail(
email,
"Payment received — thank you!",
`<h2>Payment Receipt</h2><p>Thanks for your purchase of $${(session.amount_total! / 100).toFixed(2)}.</p>`,
);
}
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
if (invoice.customer_email) {
await sendEmail(
invoice.customer_email,
"Payment failed — action required",
`<h2>Your payment didn't go through</h2><p>Please update your payment method.</p>`,
);
}
break;
}
}
return Response.json({ received: true });
},
};Error Handling
Build a safe wrapper with retry logic:
// lib/send-email-safe.ts
import { sendEmail } from "./email.ts";
interface SendResult {
success: boolean;
error?: string;
}
export async function sendEmailSafe(
to: string,
subject: string,
body: string,
): Promise<SendResult> {
try {
await sendEmail(to, subject, body);
return { success: true };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(`Email error: ${message}`);
if (message.includes("429") || message.includes("rate")) {
return { success: false, error: "Too many emails. Try again later." };
}
if (message.includes("401") || message.includes("403")) {
return { success: false, error: "Email service configuration error" };
}
return { success: false, error: "Failed to send email" };
}
}
export async function sendEmailWithRetry(
to: string,
subject: string,
body: string,
maxAttempts = 3,
): Promise<void> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await sendEmail(to, subject, body);
return;
} catch (error) {
if (attempt === maxAttempts) throw error;
const delay = Math.min(1000 * 2 ** (attempt - 1), 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}Deno KV for Rate Limiting
Deno KV is a built-in key-value store that works locally and on Deno Deploy:
// lib/rate-limit.ts
const kv = await Deno.openKv();
export async function checkRateLimit(
key: string,
max = 5,
windowMs = 60_000,
): Promise<boolean> {
const kvKey = ["rate-limit", key];
const entry = await kv.get<{ count: number; resetAt: number }>(kvKey);
const now = Date.now();
if (!entry.value || now > entry.value.resetAt) {
await kv.set(kvKey, { count: 1, resetAt: now + windowMs });
return true;
}
if (entry.value.count >= max) {
return false;
}
await kv.set(kvKey, { count: entry.value.count + 1, resetAt: entry.value.resetAt });
return true;
}Use it in your handlers:
import { checkRateLimit } from "../lib/rate-limit.ts";
// In your handler:
const ip = req.headers.get("x-forwarded-for") || "unknown";
if (!await checkRateLimit(ip)) {
return Response.json({ error: "Too many requests" }, { status: 429 });
}Production Checklist
1. Verify Your Sending Domain
| Record | Type | Purpose |
|---|---|---|
| SPF | TXT | Authorizes servers to send |
| DKIM | TXT | Cryptographic signature |
| DMARC | TXT | Policy for failed checks |
For a detailed walkthrough, see our SPF, DKIM, and DMARC setup guide.
2. Permission Flags
Lock down permissions in production:
# Only allow what's needed
deno run \
--allow-net=api.sequenzy.com,0.0.0.0:3000 \
--allow-env=SEQUENZY_API_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET \
main.ts3. Deno Deploy
Deno Deploy runs your code globally at the edge. Set environment variables in the dashboard, and deploy with:
deployctl deploy --project=your-project main.tsOn Deno Deploy, permissions are automatically granted and Deno KV works without configuration.
4. Input Validation
Deno has no built-in validation, but npm packages work:
import { z } from "npm:zod";
const contactSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
message: z.string().min(10).max(5000),
});
// In handler:
const body = await req.json();
const parsed = contactSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: parsed.error.issues[0].message }, { status: 400 });
}Beyond Transactional
const SEQUENZY_API_KEY = Deno.env.get("SEQUENZY_API_KEY")!;
export const handler: Handlers = {
async POST(req) {
const { email, name } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
await fetch("https://api.sequenzy.com/v1/subscribers", {
method: "POST",
headers: {
Authorization: `Bearer ${SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
attributes: { name },
tags: ["signed-up"],
}),
});
return Response.json({ success: true });
},
};
// Sequenzy triggers welcome sequences automatically
// when the "signed-up" tag is applied.const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY")!;
const AUDIENCE_ID = Deno.env.get("RESEND_AUDIENCE_ID")!;
export const handler: Handlers = {
async POST(req) {
const { email, name } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
const res = await fetch(`https://api.resend.com/audiences/${AUDIENCE_ID}/contacts`, {
method: "POST",
headers: {
Authorization: `Bearer ${RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, first_name: name }),
});
if (!res.ok) {
return Response.json({ error: "Failed to subscribe" }, { status: 500 });
}
return Response.json({ success: true });
},
};const SENDGRID_API_KEY = Deno.env.get("SENDGRID_API_KEY")!;
const LIST_ID = Deno.env.get("SENDGRID_LIST_ID")!;
export const handler: Handlers = {
async POST(req) {
const { email, name } = await req.json();
if (!email) {
return Response.json({ error: "Email is required" }, { status: 400 });
}
const res = await fetch("https://api.sendgrid.com/v3/marketing/contacts", {
method: "PUT",
headers: {
Authorization: `Bearer ${SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
list_ids: [LIST_ID],
contacts: [{ email, first_name: name }],
}),
});
if (!res.ok) {
return Response.json({ error: "Failed to subscribe" }, { status: 500 });
}
return Response.json({ success: true });
},
};Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one SDK. Native Stripe integration tags subscribers automatically when they purchase, cancel, or churn.
FAQ
Do I need npm packages to send emails in Deno?
No. Deno's built-in fetch is all you need. Email providers have REST APIs, one HTTP call sends an email. You only need npm packages for React Email templates or if you prefer SDK wrappers over raw fetch calls.
How do Deno permissions protect my API keys?
Deno's permission system requires you to explicitly grant network and env access. Without --allow-env, your code can't read SEQUENZY_API_KEY. Without --allow-net, it can't make HTTP requests. You can restrict both to specific values (--allow-env=SEQUENZY_API_KEY and --allow-net=api.sequenzy.com).
Can I use npm packages in Deno?
Yes. Deno supports npm packages via the npm: specifier. Use import Stripe from "npm:stripe" or add them with deno add npm:stripe. Most npm packages work out of the box.
What's the difference between Deno.serve and Fresh?
Deno.serve is the built-in HTTP server. You handle routing manually. Fresh is a full-stack framework with file-based routing, islands (interactive components), and server-side rendering. Use Deno.serve for APIs and microservices. Use Fresh for full-stack web apps.
Does Deno Deploy support Stripe webhooks?
Yes. Deno Deploy uses web-standard Request objects, so request.text() gives you the raw body for Stripe's signature verification. No special configuration needed.
How do I use Deno KV for email features?
Deno KV is perfect for rate limiting (shown above), storing email delivery status, tracking retry counts, or caching template renders. It's built in, requires no setup, and works on both local Deno and Deno Deploy.
Can I use React Email with Fresh?
Yes. Fresh uses Preact, not React, but React Email runs server-side to produce HTML strings. It doesn't interact with Fresh's rendering. Import React Email components in your server-side handler, call render(), and pass the HTML string to your email provider.
How do I handle background email sending?
For non-blocking sends, use void:
// Don't await — sends in background
void sendEmail(email, subject, body);
return Response.json({ success: true });For reliable background processing on Deno Deploy, use Deno Queues:
const kv = await Deno.openKv();
await kv.enqueue({ type: "send-email", to: email, subject, body });
// Listen for queued messages
kv.listenQueue(async (msg: { type: string; to: string; subject: string; body: string }) => {
if (msg.type === "send-email") {
await sendEmail(msg.to, msg.subject, msg.body);
}
});Wrapping Up
- Built-in
fetchmeans zero dependencies for email sending Deno.servecreates lightweight HTTP servers for APIs- Fresh provides full-stack routing with server-side form handling
- Permission flags lock down network and env access per-command
- Deno KV handles rate limiting and background queues natively
- React Email works via
npm:imports for maintainable templates - Deno Deploy runs your email code globally at the edge
Pick your provider, copy the patterns, and ship.
Frequently Asked Questions
Can I use npm email SDKs in Deno?
Yes. Deno supports npm packages via the npm: specifier (e.g., import Sequenzy from "npm:sequenzy"). Most email SDKs work out of the box since they use standard fetch internally, which Deno supports natively.
How do I handle environment variables for API keys in Deno?
Use Deno.env.get("API_KEY") to read environment variables. For local development, create a .env file and load it with --env flag (deno run --env script.ts). On Deno Deploy, set environment variables in the project dashboard.
Does Deno Deploy support email sending?
Yes. Deno Deploy supports outbound fetch requests, which is all email SDKs need. There are no cold start issues since Deploy uses V8 isolates. Just import your SDK and call it from your handler—it works the same as local development.
How do I send emails from a Deno Fresh application?
Create an API route in the routes/api/ directory and call your email SDK from the handler function. Fresh routes run server-side, so API keys stay safe. Use Fresh's Handlers type for proper request/response typing.
Can I use React Email templates with Deno?
React Email works with Deno via npm compatibility. Import the components with npm:@react-email/components and render them as you would in Node.js. Deno's JSX support handles the template compilation natively.
How do I test email sending in Deno?
Use Deno's built-in test runner (deno test) with mocks. Stub the email SDK's send function to verify it's called with correct parameters without making real API calls. Deno's testing API includes assertions and mocking utilities out of the box.
Is Deno's permission system an issue for email sending?
You need to grant network access with --allow-net for the email SDK to make HTTP requests. For environment variables, use --allow-env. In production (Deno Deploy), permissions are automatically granted. Locally, use --allow-all for development or specify exact permissions.
How do I send bulk emails without hitting Deno Deploy's request limits?
Deno Deploy has a 30-second execution limit per request. For bulk sends, use Deno's Deno.cron() (Deno KV cron) to schedule batch jobs, or use Deno Queues to process emails asynchronously across multiple invocations.
How do I handle webhook-triggered emails in Deno?
Create a handler that validates the webhook signature, parses the payload, and triggers the email send. Deno's crypto.subtle API handles HMAC signature verification natively. Return a 200 response quickly and process the email asynchronously if possible.
What's the advantage of Deno over Node.js for email sending?
Deno offers built-in TypeScript support, native fetch, and a secure-by-default permission system. For email sending specifically, the practical difference is small since both call the same HTTP APIs. The main advantage is developer experience—no tsconfig.json, no node_modules, and cleaner imports.