DEV Community

Cover image for Sending Emails with Resend
Chinwuba
Chinwuba

Posted on

Sending Emails with Resend

I run a one-person web design agency for now. Real estate clients, mostly. For a while, the contact form on my own site, the thing that actually brings me leads was wired up with EmailJS.

It worked. Until it didn't feel like enough.

The problem with EmailJS

EmailJS runs client-side. Your form calls their API directly from the browser using a public key. That's fine for a weekend project. It's not fine when:

You're rate-limited by their infra, not yours
You have zero control over deliverability
The email template lives in their dashboard's editor, and looks like every other EmailJS template on the internet
There's no way to log a submission before the send attempt, so a failed send can just... disappear

None of that is dramatic. It's just not something you want sitting between you and a lead who's deciding whether to trust you with a five-figure project.

The fix: move it server-side

I swapped EmailJS for Resend (transactional email API) + React Email (build the template as actual React components, not HTML-editor spaghetti).

  1. Install
npm install resend
npm install react-email @react-email/components -D
Enter fullscreen mode Exit fullscreen mode
  1. Structure
/server
  /emails
    /components
      Header.jsx
      Footer.jsx
    ContactConfirmation.jsx
  /api
    contact.js
Enter fullscreen mode Exit fullscreen mode

Kept this entirely on the server. The Resend API key has no business anywhere near a client bundle — that's the exact problem I was leaving behind.

3. Build the template

React Email gives you real components — Html, Body, Container, Section, Button, Hr — that compile down to email-safe HTML (inline styles, none of the CSS support you'd expect from a browser). A stripped-down shell:

import { Html, Head, Body, Container, Preview } from '@react-email/components';
import Header from './components/Header';
import EmailBody from './components/Body';
import Footer from './components/Footer';

export default function ContactConfirmation({ name }) {
  return (
    <Html>
      <Head />
      <Preview>Got your inquiry — here's what happens next</Preview>
      <Body style={main}>
        <Container style={container}>
          <Header />
          <EmailBody name={name} />
          <Footer />
        </Container>
      </Body>
    </Html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Local preview with npm run email:dev — hot reloads as you edit, so you're not blind-sending test emails to check a padding change.

  1. Wire it to Resend
import { Resend } from 'resend';
import ContactConfirmation from '../emails/ContactConfirmation';

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

export default async function handler(req, res) {
  const { name, email } = req.body;

  try {
    await resend.emails.send({
      from: 'Jeffrey at Velto <jeffrey@velto.agency>',
      to: email,
      subject: "Got your inquiry — here's what happens next",
      react: <ContactConfirmation name={name} />,
    });
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Email send failed:', error);
    res.status(500).json({ success: false });
  }
}
Enter fullscreen mode Exit fullscreen mode

Resend takes a react prop directly and handles the render server-side. No manual compile step.

  1. The Node/JSX trap

Plain Node can't parse JSX. First run threw:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".jsx"
Enter fullscreen mode Exit fullscreen mode

Fix: run the server through tsx instead of raw node — it transforms JSX on the fly via esbuild, no separate build step.

"scripts": {
  "dev": "nodemon --exec npx tsx src/index.js"
}
Enter fullscreen mode Exit fullscreen mode

Used npx in the exec command specifically because nodemon invoked directly doesn't get the node_modules/.bin PATH boost that npm run gives you — a Windows-specific gotcha that cost a few minutes.

  1. Wire the route
import express from "express";
import handler from "./routes/contact.js";

const app = express();
app.use(express.json()); // req.body is undefined without this
app.post("/api/contact", handler); // app.use() would match every HTTP verb, not just POST
app.listen(3001);
Enter fullscreen mode Exit fullscreen mode
  1. Sandbox testing

Before verifying a sending domain, Resend's sandbox address (onboarding@resend.dev) only delivers to the email you signed up with — a deliberate anti-abuse limit, not a bug. Good enough to confirm the pipeline works end to end before touching DNS records for domain verification.

Where it landed

Form submit → Express route → Resend → inbox(it landed in my spam though), fully branded, fully under my control. No public API key in the client, no fighting a WYSIWYG template editor, and a confirmation email that actually looks like it came from a person instead of a form.

Top comments (0)