Library / SDK
resend/resend-node avatar
resend/resend-node

resend-node is the Node.js SDK for the Resend email API, and its request options are the interesting part

Resend's Official Node.js SDK

971 stars98 forksTypeScriptMIT

At a glance

What is it?
resend-node is Resend's own Node.js client, shipping ESM and CommonJS builds, React email templates, inbox APIs and per-request options with cancellation. It is a thin, vendor-bound SDK, so the questions worth asking are about how it handles cancellation and what its dependency set implies.
Who is it for?
resend-node fits an application already committed to sending through Resend that wants typed request options, cancellation handling and React email templates without hand-writing request plumbing. It does not fit a team that wants a vendor-neutral mail abstraction, that runs on Node older than 20, or that needs email sending to work without a Resend account and API key.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the SDK covers and who it is for

resend-node is the official Node.js library for the Resend API. It is not a general purpose email abstraction with pluggable providers. Every namespace in it maps onto a Resend resource: emails, broadcasts, templates and inboxes. If you are building on Resend, that narrowness is the point, because the shapes match the API and the TypeScript types come with the package.

The surface visible in the documentation is small. resend.emails.send covers the common path, resend.broadcasts and resend.templates cover scheduled and reusable content, and resend.inboxes covers receiving, with threads and folders underneath. Framework guides are published for Next.js, Remix, Nuxt, Express, RedwoodJS, Hono, Bun and Astro, and separate example repositories exist for Node.js, both Next.js routers and Express.

Setup is one import and one constructor. You get an API key from the Resend dashboard, then instantiate the client with it.

js
import { Resend } from 'resend';
const resend = new Resend('re_xxxx...xxxxxx');

That key is a long-lived credential sitting in your source tree unless you put it somewhere else, and the documentation does not discuss key rotation or environment configuration, so handling that is on you.

Sending the first email returns data and error, not a thrown exception

The send call takes an options object and returns a destructurable result. The shape shown in the documentation is a pair, data and error, and the success path logs the message id.

js
const { data } = await resend.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  replyTo: '[email protected]',
  subject: 'hello world',
  text: 'it works!',
});

console.log(`Email ${data.id} has been sent`);

Two things stand out. The payload supports plain text and HTML as alternatives to each other, and replyTo is a first class field rather than something you append to the subject.

The destructuring pattern matters more. The call resolves with an object that carries either data or error, so a failed send does not throw and therefore will not be caught by a try and catch around the await. Code that only destructures data will read data.id on a failed response and fail somewhere else, far from the cause. Always check error before touching data.

Sending from your own domain requires verifying the domain in the Resend dashboard first, which the documentation calls out as a note under the first example. That is a prerequisite outside the code, and it is the kind of step that gets missed when the library is dropped into a project.

Request options ride on the last argument, and a cancel looks like a failure

Every method accepts request options as its final argument. Two of them matter most in production: signal, which cancels a request, and headers, which adds request headers.

ts
const controller = new AbortController();
await resend.emails.send({ ... }, { signal: controller.signal });

// With a timeout (Node.js 18+) and a custom header
await resend.emails.list(
  { limit: 50 },
  { signal: AbortSignal.timeout(5000), headers: { 'X-Trace-Id': traceId } },
);

The cancellation behaviour is the sharp edge. An aborted request returns a network-failure error with a null status code, which is indistinguishable from a real network failure if you only look at the error. The documented way to tell them apart is to check the status code together with signal.aborted, and explicitly not to match on the error message.

ts
const signal = AbortSignal.timeout(5000);
const { error } = await resend.emails.list({}, { signal });

if (error?.statusCode === null && signal.aborted) {
  // The request was cancelled or timed out.
}

So a timeout is not a failed request in the operational sense, and a metrics pipeline that counts every error will count your own timeouts as incidents. Handle that distinction deliberately.

The timeout API used here, AbortSignal.timeout, needs Node.js 18 or later, while the package itself declares a floor of Node 20 for consumers and 22.12 for its own development toolchain.

An optional argument in the middle forces an explicit undefined

When a method has an optional argument before the request options, the request options cannot simply be passed in its place. The documentation gives the pattern explicitly: pass undefined for the optional argument and the options object comes next.

ts
await resend.broadcasts.send(broadcastId, undefined, { signal });

This is a small ergonomic cost that follows from a positional API, and it is the kind of detail that breaks when a method signature changes between versions, because a dropped placeholder turns into a wrong argument rather than a type error in JavaScript.

Chained calls behave differently again. A chained publish() sends a separate request, so it takes its own options object rather than inheriting the one from the create call.

ts
await resend.templates
  .create({ name: 'welcome', html: '<p>Hi</p>' }, { signal })
  .publish({ signal });

Two requests, two option objects. If you are cancelling work, remember that aborting the create does not abort the publish, and vice versa.

React templates pull in an optional peer dependency

Emails can be written as a React component and passed through the react property. The template is an ordinary component that receives props.

jsx
import EmailTemplate from '../components/EmailTemplate';

const { data } = await resend.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  replyTo: '[email protected]',
  subject: 'hello world',
  react: <EmailTemplate firstName="John" product="MyApp" />,
});

console.log(`Email ${data.id} with a React template has been sent`);

The mechanism has a packaging consequence. The package manifest lists @react-email/render as a peer dependency and marks it optional, so React template support is present in the type surface without forcing every consumer of the SDK to install a renderer.

When your file has no JSX transpilation, for instance a plain .js or .ts file, the documentation points to React's jsx runtime function as the alternative to writing the element inline.

js
import { jsx } from 'react/jsx-runtime'
import EmailTemplate from '../components/EmailTemplate';

await resend.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  replyTo: '[email protected]',
  subject: 'hello world',
  react: jsx(EmailTemplate, { firstName:"John", product:"MyApp" }),
});

Note that this second form is the one place where the SDK hands a component to a renderer rather than serialising plain text, so a template that renders differently in your build than in the renderer is a bug you will only see in the recipient's inbox.

Inbox methods accept the email address instead of the id

The receiving side of the SDK is the inbox API. You create an inbox with an email address and a name, then list threads inside it.

js
const { data: inbox } = await resend.inboxes.create({
  emailAddress: '[email protected]',
  name: 'support',
});

const { data: threads } = await resend.inboxes.threads.list({
  inboxId: inbox.id,
  folders: ['inbox'],
});

Every inbox method also accepts the email address in place of the id, which saves a round trip and a stored identifier when the address is already known in your configuration.

js
const { data: threads } = await resend.inboxes.threads.list({
  emailAddress: '[email protected]',
  folders: ['inbox'],
});

await resend.inboxes.update('[email protected]', { fromName: 'Support' });

The two forms are worth understanding before you build on them. They are alternative keys for the same object, and mixing them inside one codebase is where identifier drift starts. Pick one convention and keep it.

Note also what is not here. The dependency list includes standardwebhooks, which is the library used for signing and verifying webhook payloads, yet the visible documentation covers no webhook endpoint, no event type list and no verification example. If you are building an inbound flow, that part of the surface has to be confirmed against the API documentation before you rely on it.

Dual ESM and CommonJS builds with a Node 20 floor

The package manifest is explicit about distribution. The build script produces both formats and the type declarations, and the exports map routes import and require to separate files.

json
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts"

The engines field requires Node >=20 for consumers, while devEngines asks for Node >=22.12.0 for working on the repository itself. That gap is normal for a library, but it does mean your production runtime needs a Node version at or above 20 even if your CI runs something older.

The runtime dependency list is short. postal-mime 2.7.6 handles MIME work, standardwebhooks 1.1.1 handles webhook signatures, and @react-email/render is the optional peer. The heavy lifting on the HTTP side is left to the platform fetch, which is why the cancellation story is built on AbortController rather than on a client with its own request lifecycle.

The repository itself is a pnpm workspace with Biome for linting, vitest for unit tests, and a separate e2e suite. The e2e suite uses a record mode that removes saved recordings and re-records them, which is how the project tests against the live API without hitting it on every run. The e2e directory has its own Next.js integration project, so framework compatibility is checked rather than assumed.

Where this SDK is the wrong tool

The obvious limitation is vendor binding. Every method, type and error path assumes Resend. If a requirement later says mail must go through another provider, or through an internal relay, this SDK gives you no abstraction to break at. The alternative in that case is to call the HTTP API directly with fetch, which is a small amount of work for a single endpoint and gives you full control over timeouts, retries and payloads, but you give up the typed request options, the React template support and the consistent data and error shape.

A second limitation is error surface coverage. The documentation shows one error field with a status code and mentions a null status code for aborts. It does not document the full error type, the rate limit behaviour, the retry semantics, or what happens when the API returns a validation failure. You can work with the SDK, but you are relying on behaviour that the visible documentation does not pin down.

Third, the API key handling is entirely on the consumer. The constructor takes a key string, and nothing in the documentation covers reading it from configuration, rotating it, or using a different key per environment.

Finally, this is a fast moving package. The version in the manifest is 6.34.1, and the three most recent releases all landed within a few days of each other in October 2026, with the last push on 2026-10-16. That pace means the changelog deserves a read before each upgrade, because method signatures change, as the placeholder example shows.

Editorial conclusion

resend-node fits an application already committed to sending through Resend that wants typed request options, cancellation handling and React email templates without hand-writing request plumbing. It does not fit a team that wants a vendor-neutral mail abstraction, that runs on Node older than 20, or that needs email sending to work without a Resend account and API key. Before adopting it, verify that the Node version in your production runtime satisfies the >=20 engine requirement, install @react-email/render deliberately if you plan to use the react property, and test the abort path yourself so you can tell a cancelled request apart from a genuine failure.

Frequently asked questions

How do I install resend-node and send my first email?

Install the package with npm install resend or yarn add resend, create the client with new Resend('re_xxxx...xxxxxx'), then call resend.emails.send with from, to, subject and a text or html body. Sending from your own domain requires verifying it in the Resend dashboard first.

How do I tell an aborted request apart from a failed one in resend-node?

An aborted request returns a network-failure error with a null status code, so check the status code together with signal.aborted rather than matching on the error message.

Does resend-node support CommonJS require?

Yes. The package ships both formats, with the exports map routing require to dist/index.cjs and import to dist/index.mjs, and the engines field requires Node >=20.

Do I need to install anything extra to send React email templates?

The package manifest lists @react-email/render as an optional peer dependency, so React template support is not forced on every install. If your file has no JSX transpilation, the documentation shows using jsx from react/jsx-runtime instead.

Can I list inbox threads without knowing the inbox id?

Yes. Every inbox method also accepts the inbox email address in place of its id, so threads.list accepts either inboxId or emailAddress.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. resend/resend-node on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/resend-resend-node.svg)](https://hysenlabs.com/projects/resend-resend-node)

Was this useful?