mailgun-js vs nodemailer vs sendgrid
Sending Transactional Emails in Node.js Backend Services
mailgun-jsnodemailersendgridSimilar Packages:

Sending Transactional Emails in Node.js Backend Services

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
mailgun-js0892-527 years agoMIT
nodemailer017,541542 kB016 days agoMIT-0
sendgrid03,048-929 years agoMIT

Mailgun vs Nodemailer vs SendGrid: Architecture, Maintenance, and Implementation

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.

πŸ—οΈ Architecture: SMTP Relay vs HTTP API

nodemailer uses the SMTP protocol.

  • It acts as a universal client that can talk to any SMTP server (Gmail, AWS SES, SendGrid SMTP, etc.).
  • You manage the connection pool and transport layer.
// 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.

  • It bypasses SMTP entirely, sending JSON payloads directly to Mailgun's servers.
  • Requires the specific Mailgun SDK (note: 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.

  • Like Mailgun, it sends HTTP requests rather than SMTP packets.
  • Note: The 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');

βš™οΈ Configuration & Setup Complexity

Setting up these libraries involves different security and configuration considerations.

nodemailer requires SMTP credentials.

  • You need a host, port, and authentication details.
  • Great for local development using tools like 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.

  • Configuration is tied to your Mailgun account settings.
  • No need to manage SMTP ports or TLS settings manually.
// mailgun-js: Domain configuration
const mg = mailgun({
  apiKey: process.env.MAILGUN_API_KEY,
  domain: 'mg.yourdomain.com'
});

sendgrid requires a Single API Key.

  • Simpler than SMTP, but you must verify your sender identity in the dashboard.
  • The legacy package lacks some modern TypeScript definitions found in @sendgrid/mail.
// sendgrid: API Key only
const sg = sendgrid(process.env.SENDGRID_API_KEY);

πŸ“¬ Sending a Basic Email

The developer experience differs when constructing and sending the actual message.

nodemailer uses a sendMail method with a consistent object structure.

  • Works the same way regardless of the underlying SMTP provider.
  • Supports callbacks or Promises.
// 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.

  • Parameters map directly to Mailgun's API fields.
  • Returns a Promise with API response details.
// 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.

  • Requires constructing a Mail object or a specific JSON structure.
  • The legacy API differs slightly from the modern @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);

πŸ“Ž Attachments & Templates

Handling binary data and dynamic content is critical for transactional emails.

nodemailer handles attachments via buffers or streams.

  • You can attach files from the local filesystem or URLs.
  • Templates are usually managed in your code (e.g., Handlebars).
// nodemailer: Attachments
await transporter.sendMail({
  attachments: [{ path: '/path/to/file.pdf' }]
});

mailgun-js supports attachments and stored templates.

  • You can reference templates created in the Mailgun dashboard by name.
  • Attachments are passed as file objects or buffers.
// 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.

  • Uses Handlebars syntax directly in the dashboard.
  • Pass substitution data in the 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);

⚠️ Maintenance & Deprecation Status

This is the most critical factor for architectural decisions.

nodemailer is actively maintained.

  • It is the de facto standard for Node.js email.
  • Safe to use in new projects.
// nodemailer: Current Status
// βœ… Active Maintenance
// βœ… High Community Support

mailgun-js is legacy.

  • The official SDK is now mailgun.js (lowercase 'js').
  • Using the old package may lead to missing features or security patches.
// mailgun-js: Current Status
// ⚠️ Legacy / Superseded
// βœ… Use 'mailgun.js' instead

sendgrid is deprecated.

  • Twilio SendGrid officially recommends @sendgrid/mail.
  • The old package no longer receives feature updates.
// sendgrid: Current Status
// ❌ Deprecated
// βœ… Use '@sendgrid/mail' instead

πŸ“Š Summary: Key Differences

Featurenodemailermailgun-jssendgrid
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

πŸ’‘ The Big Picture

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.

How to Choose: mailgun-js vs nodemailer vs sendgrid

  • mailgun-js:

    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.

  • nodemailer:

    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.

  • sendgrid:

    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.

README for mailgun-js

mailgun.js

Simple Node.js module for Mailgun.

npm version JavaScript Style Guide License Donate Buy me a coffee

Installation

npm install mailgun-js

Usage overview

This is a simple Node.js module for interacting with the Mailgun API. This module is intended to be used within Node.js environment and not from the browser. For browser use the mailgun.js module.

Please see Mailgun Documentation for full Mailgun API reference.

This module works by providing proxy objects for interacting with different resources through the Mailgun API. Most methods take a data parameter, which is a Javascript object that would contain the arguments for the Mailgun API. All methods take a final parameter callback with two parameters: error, and body. We try to parse the body into a javascript object, and return it to the callback as such for easier use and inspection by the client. If there was an error a new Error object will be passed to the callback in the error parameter. If the error originated from the (Mailgun) server, the response code will be available in the statusCode property of the error object passed in the callback. See the /docs folder for detailed documentation. For full usage examples see the /test folder.

var api_key = 'XXXXXXXXXXXXXXXXXXXXXXX';
var domain = 'www.mydomain.com';
var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});

var data = {
  from: 'Excited User <me@samples.mailgun.org>',
  to: 'serobnic@mail.ru',
  subject: 'Hello',
  text: 'Testing some Mailgun awesomeness!'
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

Note that the to field is required and should be a string containing 1 or more comma-separated addresses. Additionally cc and bcc fields can be specified. Recipients in those fields will be addressed as such. See https://documentation.mailgun.com/api-sending.html#sending for additional details.

Messages stored using the Mailgun store() action can be retrieved using messages(<message_key>).info() function. Optionally the MIME representation of the message can be retrieved if MIME argument is passed in and set to true.

Something more elaborate. Get mailing list info, create a member and get mailing list members and update member. Notice that the proxy objects can be reused.

var list = mailgun.lists('mylist@mycompany.com');

list.info(function (err, data) {
  // `data` is mailing list info
  console.log(data);
});

var bob = {
  subscribed: true,
  address: 'bob@gmail.com',
  name: 'Bob Bar',
  vars: {age: 26}
};

list.members().create(bob, function (err, data) {
  // `data` is the member details
  console.log(data);
});

list.members().list(function (err, members) {
  // `members` is the list of members
  console.log(members);
});

list.members('bob@gmail.com').update({ name: 'Foo Bar' }, function (err, body) {
  console.log(body);
});

list.members('bob@gmail.com').delete(function (err, data) {
  console.log(data);
});

Options

Mailgun object constructor options:

  • apiKey - Your Mailgun API KEY
  • publicApiKey - Your public Mailgun API KEY
  • domain - Your Mailgun Domain (Please note: domain field is MY-DOMAIN-NAME.com, not https://api.mailgun.net/v3/MY-DOMAIN-NAME.com)
  • mute - Set to true if you wish to mute the console error logs in validateWebhook() function
  • proxy - The proxy URI in format http[s]://[auth@]host:port. ex: 'http://proxy.example.com:8080'
  • timeout - Request timeout in milliseconds
  • host - the mailgun host (default: 'api.mailgun.net'). Note that if you are using the EU region the host should be set to 'api.eu.mailgun.net'
  • protocol - the mailgun protocol (default: 'https:', possible values: 'http:' or 'https:')
  • port - the mailgun port (default: '443')
  • endpoint - the mailgun host (default: '/v3')
  • retry - the number of total attempts to do when performing requests. Default is 1. That is, we will try an operation only once with no retries on error. You can also use a config object compatible with the async library for more control as to how the retries take place. See docs here
  • testMode - turn test mode on. If test mode is on, no requests are made, rather the request options and data is logged
  • testModeLogger - custom test mode logging function

Attachments

Attachments can be sent using either the attachment or inline parameters. inline parameter can be use to send an attachment with inline disposition. It can be used to send inline images. Both types are supported with same mechanisms as described, we will just use attachment parameter in the documentation below but same stands for inline.

Sending attachments can be done in a few ways. We can use the path to a file in the attachment parameter. If the attachment parameter is of type string it is assumed to be the path to a file.

var filepath = path.join(__dirname, 'mailgun_logo.png');

var data = {
  from: 'Excited User <me@samples.mailgun.org>',
  to: 'serobnic@mail.ru',
  subject: 'Hello',
  text: 'Testing some Mailgun awesomeness!',
  attachment: filepath
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

We can pass a buffer (has to be a Buffer object) of the data. If a buffer is used the data will be attached using a generic filename "file".

var filepath = path.join(__dirname, 'mailgun_logo.png');
var file = fs.readFileSync(filepath);

var data = {
  from: 'Excited User <me@samples.mailgun.org>',
  to: 'serobnic@mail.ru',
  subject: 'Hello',
  text: 'Testing some Mailgun awesomeness!',
  attachment: file
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

We can also pass in a stream of the data. This is useful if you're attaching a file from the internet.

var request = require('request');
var file = request("https://www.google.ca/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png");

var data = {
  from: 'Excited User <me@samples.mailgun.org>',
  to: 'serobnic@mail.ru',
  subject: 'Hello',
  text: 'Testing some Mailgun awesomeness!',
  attachment: file
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

Finally we provide a Mailgun.Attachment class to add attachments with a bit more customization. The Attachment constructor takes an options object. The options parameters can have the following fields:

  • data - can be one of
    • a string representing file path to the attachment
    • a buffer of file data
    • an instance of Stream which means it is a readable stream.
  • filename - the file name to be used for the attachment. Default is 'file'
  • contentType - the content type. Required for case of Stream data. Ex. image/jpeg.
  • knownLength - the content length in bytes. Required for case of Stream data.

If an attachment object does not satisfy those valid conditions it is ignored. Multiple attachments can be sent by passing an array in the attachment parameter. The array elements can be of any one of the valid types and each one will be handled appropriately.

var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});
var filename = 'mailgun_logo.png';
var filepath = path.join(__dirname, filename);
var file = fs.readFileSync(filepath);

var attch = new mailgun.Attachment({data: file, filename: filename});

var data = {
  from: 'Excited User <me@samples.mailgun.org>',
  to: 'serobnic@mail.ru',
  subject: 'Hello',
  text: 'Testing some Mailgun awesomeness!',
  attachment: attch
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});
var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});
var filename = 'mailgun_logo.png';
var filepath = path.join(__dirname, filename);
var fileStream = fs.createReadStream(filepath);
var fileStat = fs.statSync(filepath);

msg.attachment = new mailgun.Attachment({
  data: fileStream,
  filename: 'my_custom_name.png',
  knownLength: fileStat.size,
  contentType: 'image/png'});

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

Sending MIME messages

Sending messages in MIME format can be accomplished using the sendMime() function of the messages() proxy object. The data parameter for the function has to have to and message properties. The message property can be a full file path to the MIME file, a stream of the file, or a string representation of the MIME message. To build a MIME string you can use the nodemailer library. Some examples:

var domain = 'mydomain.org';
var mailgun = require('mailgun-js')({ apiKey: "YOUR API KEY", domain: domain });
var MailComposer = require('nodemailer/lib/mail-composer');

var mailOptions = {
  from: 'you@samples.mailgun.org',
  to: 'mm@samples.mailgun.org',
  subject: 'Test email subject',
  text: 'Test email text',
  html: '<b> Test email text </b>'
};

var mail = new MailComposer(mailOptions);

mail.compile().build((err, message) => {

    var dataToSend = {
        to: 'mm@samples.mailgun.org',
        message: message.toString('ascii')
    };

    mailgun.messages().sendMime(dataToSend, (sendError, body) => {
        if (sendError) {
            console.log(sendError);
            return;
        }
    });
});

Referencing MIME file

var filepath = '/path/to/message.mime';

var data = {
  to: fixture.message.to,
  message: filepath
};

mailgun.messages().sendMime(data, function (err, body) {
  console.log(body);
});
var filepath = '/path/to/message.mime';

var data = {
  to: fixture.message.to,
  message: fs.createReadStream(filepath)
};

mailgun.messages().sendMime(data, function (err, body) {
  console.log(body);
});

Creating mailing list members

members().create({data}) will create a mailing list member with data. Mailgun also offers a resource for creating members in bulk. Doing a POST to /lists/<address>/members.json adds multiple members, up to 1,000 per call, to a Mailing List. This can be accomplished using members().add().

var members = [
  {
    address: 'Alice <alice@example.com>',
    vars: { age: 26 }
  },
  {
    name: 'Bob',
    address: 'bob@example.com',
    vars: { age: 34 }
  }
];

mailgun.lists('mylist@mycompany.com').members().add({ members: members, subscribed: true }, function (err, body) {
  console.log(body);
});

Generic requests

Mailgun-js also provides helper methods to allow users to interact with parts of the api that are not exposed already. These are not tied to the domain passed in the constructor, and thus require the full path with the domain passed in the resource argument.

  • mailgun.get(resource, data, callback) - sends GET request to the specified resource on api.
  • mailgun.post(resource, data, callback) - sends POST request to the specified resource on api.
  • mailgun.delete(resource, data, callback) - sends DELETE request to the specified resource on api.
  • mailgun.put(resource, data, callback) - sends PUT request to the specified resource on api.

Example: Get some stats

mailgun.get('/samples.mailgun.org/stats', { event: ['sent', 'delivered'] }, function (error, body) {
  console.log(body);
});

Promises

Module works with Node-style callbacks, but also implements promises with the promisify-call library.

mailgun.lists('mylist@mydomain.com').info().then(function (data) {
  console.log(data);
}, function (err) {
  console.log(err);
});

The function passed as 2nd argument is optional and not needed if you don't care about the fail case.

Webhook validation

The Mailgun object also has a helper function for validating Mailgun Webhook requests (as per the mailgun docs for securing webhooks). This code came from this gist.

Example usage:

var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});

function router(app) {
  app.post('/webhooks/mailgun/*', function (req, res, next) {
    var body = req.body;

    if (!mailgun.validateWebhook(body.timestamp, body.token, body.signature)) {
      console.error('Request came, but not from Mailgun');
      res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
      return;
    }

    next();
  });

  app.post('/webhooks/mailgun/catchall', function (req, res) {
    // actually handle request here
  });
}

Email Addresses validation

These routes require Mailgun public API key. Please check Mailgun email validation documentation for more responses details.

Validate Email Address

mailgun.validate(address, private, options, fn)

Checks if email is valid.

  • private - whether it's private validate
  • options - any additional options

Example usage:

var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});

mailgun.validate('test@mail.com', function (err, body) {
  if(body && body.is_valid){
    // do something
  }
});

Parse Email Addresses list

Parses list of email addresses and returns two lists:

  • parsed email addresses
  • unparseable email addresses

Example usage:

var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});

mailgun.parse([ 'test@mail.com', 'test2@mail.com' ], function (err, body) {
  if(error){
    // handle error
  }else{
    // do something with parsed addresses: body.parsed;
    // do something with unparseable addresses: body.unparseable;
  }
});

Debug logging

debug package is used for debug logging.

DEBUG=mailgun-js node app.js

Test mode

Test mode can be turned on using testMode option. When on, no requests are actually sent to Mailgun, rather we log the request options and applicable payload and form data. By default we log to console.log, unless DEBUG is turned on, in which case we use debug logging.

mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain, testMode: true })

const data = {
  from: 'mailgunjs+test1@gmail.com',
  to: 'mailgunjstest+recv1@gmail.com',
  subject: 'Test email subject',
  text: 'Test email text'
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});
options: { hostname: 'api.mailgun.net',
  port: 443,
  protocol: 'https:',
  path: '/v3/sandbox12345.mailgun.org/messages',
  method: 'POST',
  headers:
   { 'Content-Type': 'application/x-www-form-urlencoded',
     'Content-Length': 127 },
  auth: 'api:key-0e8pwgtt5ylx0m94xwuzqys2-o0x4-77',
  agent: false,
  timeout: undefined }
payload: 'to=mailgunjs%2Btest1%40gmail.com&from=mailgunjstest%2Brecv1%40gmail.com&subject=Test%20email%20subject&text=Test%20email%20text'
form: undefined
undefined

Note that in test mode no error or body are returned as a result.

The logging can be customized using testModeLogger option which is a function to perform custom logging.

const logger = (httpOptions, payload, form) => {
  const { method, path } = httpOptions
  const hasPayload = !!payload
  const hasForm = !!form

  console.log(`%s %s payload: %s form: %s`, method, path, hasPayload, hasForm)
}

mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain, testMode: true, testModeLogger: logger })

const data = {
  from: 'mailgunjs+test1@gmail.com',
  to: 'mailgunjstest+recv1@gmail.com',
  subject: 'Test email subject',
  text: 'Test email text'
};

mailgun.messages().send(data, function (error, body) {
  console.log(body);
});

Sample output:

POST /v3/sandbox12345.mailgun.org/messages payload: true form: false
undefined

Tests

To run the test suite you must first have a Mailgun account with a domain setup. Then create a file named ./test/data/auth.json, which contains your credentials as JSON, for example:

{ "api_key": "XXXXXXXXXXXXXXXXXXXXXXX", "public_api_key": "XXXXXXXXXXXXXXXXXXXXXXX", "domain": "mydomain.mailgun.org" }

You should edit ./test/data/fixture.json and modify the data to match your context.

Then install the dev dependencies and execute the test suite:

$ npm install
$ npm test

The tests will call Mailgun API, and will send a test email, create route(s), mailing list and mailing list member.

Notes

This project is not endorsed by or affiliated with Mailgun. The general design and some code was heavily inspired by node-heroku-client.

License

Copyright (c) 2012 - 2017 OneLobby and Bojan D.

Licensed under the MIT License.