nodemailer is the most popular module for Node.js applications, allowing developers to send emails via SMTP or through compatible transport mechanisms. mailgun-js and sendgrid are client libraries for specific Email Service Providers (ESPs), enabling email delivery via HTTP APIs with advanced features like tracking and templates. While nodemailer offers provider flexibility, the ESP-specific libraries provide deeper integration with their respective platforms' analytics and management tools.
Sending email from a Node.js application is a common requirement, but the approach varies significantly depending on whether you use a general-purpose SMTP library or a provider-specific API client. nodemailer, mailgun-js, and sendgrid represent these two distinct architectural patterns. Let's examine how they handle configuration, delivery, and long-term maintenance.
nodemailer uses the SMTP protocol.
// nodemailer: SMTP Transport
const transporter = nodemailer.createTransport({
host: 'smtp.sendgrid.net',
port: 587,
auth: { user: 'apikey', pass: 'YOUR_SENDGRID_API_KEY' }
});
mailgun-js uses Mailgun's HTTP REST API.
mailgun.js is the current version).// mailgun-js: HTTP API Client
const mailgun = require('mailgun-js');
const mg = mailgun({ apiKey: 'YOUR_API_KEY', domain: 'YOUR_DOMAIN' });
sendgrid uses SendGrid's HTTP Web API.
sendgrid package is deprecated; @sendgrid/mail is the modern standard.// sendgrid: HTTP API Client (Legacy Package)
const sendgrid = require('sendgrid');
const sg = sendgrid('YOUR_API_KEY');
Setting up these libraries involves different security and configuration considerations.
nodemailer requires SMTP credentials.
ethereal.email.// nodemailer: Dev-friendly test account
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: { user: '...', pass: '...' }
});
mailgun-js requires API Keys and Domain.
// mailgun-js: Domain configuration
const mg = mailgun({
apiKey: process.env.MAILGUN_API_KEY,
domain: 'mg.yourdomain.com'
});
sendgrid requires a Single API Key.
@sendgrid/mail.// sendgrid: API Key only
const sg = sendgrid(process.env.SENDGRID_API_KEY);
The developer experience differs when constructing and sending the actual message.
nodemailer uses a sendMail method with a consistent object structure.
// nodemailer: Sending mail
await transporter.sendMail({
from: '"Sender" <sender@domain.com>',
to: 'recipient@example.com',
subject: 'Hello',
text: 'Hello world',
html: '<b>Hello world</b>'
});
mailgun-js uses a messages.create method.
// mailgun-js: Sending mail
const data = {
from: 'Excited User <mailgun@yourdomain.com>',
to: 'recipient@example.com',
subject: 'Hello',
text: 'Testing some Mailgun awesomness!'
};
mg.messages().send(data);
sendgrid uses a send method with a specific helper format.
Mail object or a specific JSON structure.@sendgrid/mail helper.// sendgrid: Sending mail
const request = sg.emptyRequest({
method: 'POST',
path: '/v3/mail/send',
body: {
personalizations: [{ to: [{ email: 'recipient@example.com' }] }],
from: { email: 'sender@domain.com' },
subject: 'Hello',
content: [{ type: 'text/plain', value: 'Hello' }]
}
});
sg.API(request);
Handling binary data and dynamic content is critical for transactional emails.
nodemailer handles attachments via buffers or streams.
// nodemailer: Attachments
await transporter.sendMail({
attachments: [{ path: '/path/to/file.pdf' }]
});
mailgun-js supports attachments and stored templates.
// mailgun-js: Templates
const data = {
to: 'recipient@example.com',
template: 'welcome_email',
'o:tracking-clicks': 'yes'
};
mg.messages().send(data);
sendgrid has a robust Dynamic Templates system.
dynamic_template_data field.// sendgrid: Dynamic Templates
const msg = {
to: 'recipient@example.com',
templateId: 'd-your_template_id',
dynamic_template_data: { first_name: 'John' }
};
sg.send(msg);
This is the most critical factor for architectural decisions.
nodemailer is actively maintained.
// nodemailer: Current Status
// β
Active Maintenance
// β
High Community Support
mailgun-js is legacy.
mailgun.js (lowercase 'js').// mailgun-js: Current Status
// β οΈ Legacy / Superseded
// β
Use 'mailgun.js' instead
sendgrid is deprecated.
@sendgrid/mail.// sendgrid: Current Status
// β Deprecated
// β
Use '@sendgrid/mail' instead
| Feature | nodemailer | mailgun-js | sendgrid |
|---|---|---|---|
| Protocol | π‘ SMTP | π HTTP API | π HTTP API |
| Provider | π Any (Agnostic) | π Mailgun Only | π SendGrid Only |
| Status | β Active | β οΈ Legacy (mailgun.js) | β Deprecated (@sendgrid/mail) |
| Templates | π» Code-based | π¨ Dashboard Managed | π¨ Dynamic Templates |
| Tracking | βοΈ Manual/Plugin | π Built-in | π Built-in |
nodemailer is the Swiss Army Knife πͺβit works everywhere and lets you swap providers easily. It is the safest bet for general-purpose Node.js applications where vendor lock-in is a concern.
mailgun-js (and its successor mailgun.js) is the Specialist π―βideal if you need Mailgun's specific features like inbound parsing and detailed logs. Ensure you use the modern SDK.
sendgrid (and its successor @sendgrid/mail) is the Enterprise Choice π’βbest for high-volume sending with strict deliverability needs and advanced template management. Avoid the deprecated package.
Final Thought: For new projects, avoid sendgrid and mailgun-js directly. Use nodemailer for SMTP flexibility, or the modern SDKs (@sendgrid/mail, mailgun.js) if you need specific API features. Architecture is about longevityβchoose maintained tools.
Choose nodemailer if you need flexibility to switch email providers without changing code, or if you prefer using SMTP credentials (e.g., AWS SES, Gmail, or a self-hosted server). It is the industry standard for Node.js and is ideal for applications that require a transport-agnostic solution or need to run in environments where HTTP API calls are restricted.
Note that mailgun-js is considered legacy; the modern SDK is mailgun.js. Choose the Mailgun ecosystem if you need robust email analytics, inbound routing, and a developer-friendly API. It is best for teams already invested in the Mailgun platform who want tight integration with dashboard-managed templates and event webhooks.
Note that the sendgrid package is deprecated in favor of @sendgrid/mail. Choose the SendGrid ecosystem if you require high deliverability rates, advanced marketing features, and a powerful template engine. It is suitable for applications that need detailed open/click tracking and seamless integration with Twilio's communication suite.
Send emails from Node.js β easy as cake! π°βοΈ
See nodemailer.com for documentation and terms.
Nodemailer is developed by the team behind EmailEngine, a self-hosted email API that turns any Gmail, Microsoft 365, or IMAP account into a REST endpoint, with managed OAuth2, webhooks for incoming mail, and built-in sending. If you would rather call an HTTP API than maintain IMAP and SMTP connections yourself, that is what it is for.
Nodemailer targets Node.js 20 and later. The ES module build also runs on Bun (tested against the latest release) and on Cloudflare Workers with the nodejs_compat compatibility flag. On Workers only the SMTP based transports apply, sendmail needs a child process, and the runtime does not allow turning certificate validation off, so tls.rejectUnauthorized: false fails there with an error.
Documentation for Nodemailer can be found at nodemailer.com.
Nodemailer 10 and later require Node.js 20 or newer. The package ships both ES module and CommonJS builds, so import nodemailer from 'nodemailer' and const nodemailer = require('nodemailer') both work. If you need to run on an older Node.js version, stay on the 9.x line.
Gmail either works well, or it does not work at all. It is probably easier to switch to an alternative service instead of fixing issues with Gmail. If Gmail does not work for you, then don't use it. Read more about it here. If the blocker is OAuth2 setup rather than Gmail itself, EmailEngine handles the OAuth2 flow and token refresh for you.
Check your firewall settings. Timeout usually occurs when you try to open a connection to a firewalled port either on the server or on your machine. Some ISPs also block email ports to prevent spamming.
It's either a firewall issue, or your SMTP server blocks authentication attempts from some servers.
secure option. This should be set to true only for port 465. For every other port, it should be false. Setting it to false does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.false to skip chain verification or upgrade your Node versionlet configOptions = {
host: 'smtp.example.com',
port: 587,
tls: {
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
}
};
Node.js uses c-ares to resolve domain names, not the DNS library provided by the system, so if you have some custom DNS routing set up, it might be ignored. Nodemailer runs dns.resolve4() and dns.resolve6() to resolve hostname into an IP address. If both calls fail, then Nodemailer will fall back to dns.lookup(). If this does not work for you, you can hard code the IP address into the configuration like shown below. In that case, Nodemailer would not perform any DNS lookups.
let configOptions = {
host: '1.2.3.4',
port: 465,
secure: true,
tls: {
// must provide server name, otherwise TLS certificate check will fail
servername: 'example.com'
}
};
Nodemailer 10 and later are written in TypeScript and ship their own type definitions, so @types/nodemailer is no longer needed and should be removed from your project to avoid conflicting declarations. The type names follow the layout of the old definitions, so references such as Mail.Options, SMTPTransport.Options or Transporter<SMTPTransport.SentMessageInfo> keep compiling, and the most used types (SendMailOptions, Transporter, SentMessageInfo, Attachment, Address) are exported from the package root. For older Nodemailer versions, the community maintained type definitions still apply.
If you are having issues with Nodemailer, then the best way to find help would be Stack Overflow or revisit the docs.
Nodemailer is licensed under the MIT No Attribution license
The Nodemailer logo was designed by Sven Kristjansen.