Most email tutorials start with how to set up SMTP or which library to install. But few explain the real challenges — why your emails might still land in spam, what breaks at scale, or how APIs outperform traditional setups in real-world apps. If you’ve ever had an email silently fail, or watched a bulk send crawl because of SMTP rate limits, you already know the pain. Here, we’ll walk through practical ways to send emails from Node.js — from small transactional messages to high-volume, personalized delivery using both SMTP and modern APIs. You’ll see exactly where each method works best, what to watch out for, and how to avoid common pitfalls that tutorials usually skip.
Overview of Email Sending Methods in Node.js
There are two main ways to send email with Node.js: using SMTP or using an Email API. SMTP is a well-supported and reliable method, but it can be harder to scale up. If you want to send thousands of emails at once, or if you need delivery tracking and analytics, using SMTP alone might not be enough.
SMTP vs Email API: What’s the Difference?
When you send an email using SMTP, your Node.js app connects to a designated SMTP server and gives it the email details (like who it's from, who it's to, the subject, and the body). The server then transfers the message to the recipient's email server. The SMTP protocol is robust, time-tested and easy to troubleshoot.
To send email using node.js and SMTP, you use a library like Nodemailer. You also need access to a working SMTP server. This can either be an external SMTP server provided by UniOne, Gmail, Outlook, etc., or your own one. If you ever need to change to a new server, you’ll only need to enter the new credentials.
Email APIs are modern alternatives to SMTP. Instead of connecting to a mail server, your app sends a POST request to an HTTP API endpoint, with all the email information in JSON format. The email service provider receives this request and handles the delivery.
Many services, including UniOne, offer email APIs that are easy to use, outperform SMTP in speed, and offer extra features like real-time status tracking, bulk email support, template management, and error logging. However, unlike SMTP, email APIs are not standardized, so you cannot easily migrate from one API to another.
Below’s a short comparison of both methods:
Feature |
SMTP |
Email API |
Protocol |
SMTP (port 587, 465, or 25) |
HTTPS |
Setup Complexity |
Easy |
Medium |
Speed |
Slower |
Faster |
Delivery Tracking |
Not built-in |
Built-in |
Scalability |
Limited |
High |
Supported by UniOne |
Yes (SMTP Service) |
|
Best For |
Simple apps, legacy systems |
Modern apps, large volumes, automation |
Choosing the Right Method for Your Node.js Application
The best method depends on what your app needs. Here are key questions to ask yourself:
- Do you need fast delivery and tracking? If yes, go with an Email API. APIs give you fast delivery and real-time status updates.
- Do you already use SMTP in your system? If yes, and you don’t need extra features, you can stick with SMTP. It’s reliable and well-supported in Node.js.
- Will you send a lot of emails? If yes, Email APIs are the way to go. They are built for bulk email and handle throttling, retries, and analytics.
- Do you want to send personalized emails with templates? Both methods are ok, but APIs usually offer more advanced tools for managing and reusing templates.
- Are you sending transactional or marketing emails?
- For transactional (password reset, order confirmation), either method works.
- For marketing campaigns or bulk emails, APIs are safer and easier to manage.
In most modern projects, email APIs are the preferred choice because they are faster, more reliable, and provide more control over delivery.
Basic Requirements Before You Start
Before you can send email, your project must be correctly configured. Make sure you have:
A verified sender email address |
You need an email that is allowed to send messages. Most services won’t deliver emails from fake or unverified addresses. |
Domain authentication (DKIM, SPF, DMARC) |
To prevent your emails from being marked as spam, your sending domain must be authenticated. Most providers (like UniOne) provide DNS setup instructions. |
A Node.js project |
Your app must be set up with npm or yarn. If not, run npm init -y to create a project. |
A mail service provider account |
You need access to either: • an SMTP server (like the one UniOne offers), or • an email API service (like UniOne's API platform). |
Your credentials |
You must have either: • SMTP username and password, • or API user id and key/token for authentication. |
A tested internet connection |
Without stable network access, delivery can fail or hang. |
Installed dependencies |
• For SMTP: install Nodemailer • For API: install the axios package or use built-in https module. |
Basic knowledge of async code |
Sending emails is an asynchronous task. You must use async/await or .then() to handle results and errors properly. |
How to Send Email Using SMTP in Node.js
Setting Up Nodemailer
Nodemailer is a Node.js library that allows you to send emails using SMTP. It is the most popular tool for this purpose with all standard features needed for sending transactional or notification emails.
To use Nodemailer, install it in your project. In your terminal, run this command:
npm install nodemailer |
Once installed, you need to import it in your code:
const nodemailer = require('nodemailer'); |
Nodemailer works by creating a transporter. This transporter is an object that knows how to connect to the SMTP server and send emails.
You must configure this transporter with the SMTP host name or IP, port number and authentication credentials (username and password). A basic configuration looks like this:
const transporter = nodemailer.createTransport({ host: 'smtp.example.com', port: 587, secure: false, // true for port 465, false for 587 auth: { user: 'your_username', pass: 'your_password' } }); |
The secure option should be true only if you use port 465 with implicit TLS. For port 587 with STARTTLS support (recommended), use false.
Connecting to an SMTP Server
To send emails using SMTP, you must connect to an actual SMTP server. This server can be provided by an external service (such as UniOne) or hosted by you. Below, we use a server provided by UniOne. For details on setting up your account, visit UniOne’s official SMTP Service page.
For authorization settings, you need to know your user ID and API key (which serves as a password for SMTP connection). The user ID can be seen at the top left of your dashboard screen. For the API key, follow the steps below:
- Sign in to your UniOne account.
- Go to the Account – Security section.
- Create a new token or copy an existing one.
- Save it securely. You will use this token as part of the authorization header in your HTTP request.
Authentication settings for UniOne: user id and API key.
Example setup using UniOne:
const transporter = nodemailer.createTransport({ host: 'smtp.unione.io', port: 587, secure: false, auth: { user: 'your_unione_user_id', pass: 'your_unione_api_key’' } }); |
Make sure to use your real UniOne credentials. You can find this information in your UniOne dashboard after registering.
To verify the connection before sending an email, you can run:
transporter.verify(function(error, success) { if (error) { console.error('SMTP connection failed:', error); } else { console.log('SMTP server is ready to take messages'); } }); |
This helps check your credentials and connection settings before you start sending emails.
Sending a Plain Text Email
The simplest way to send an email is using plain text. This is useful for alerts, system logs, or other text-only messages.
Here is how to send a plain text message using Nodemailer:
const mailOptions = { from: '"Your App" <your_email@example.com>', to: 'recipient@example.com', subject: 'Test Email', text: 'Hello! This is a plain text email sent from Node.js using Nodemailer.' }; transporter.sendMail(mailOptions, function(error, info) { if (error) { return console.error('Error while sending:', error); } console.log('Message sent: %s', info.messageId); }); |
Where:
- from is your verified sender address;
- to is the recipient email;
- subject is the email subject line;
- text is the plaintext message body.
This method sends a basic message without any visual styling.
Sending an HTML Email
HTML emails allow you to use rich formatting, colors, links, and images. This is useful for newsletters, confirmations, or branded communication.
To send HTML content, replace the text field with an html field:
const mailOptions = { from: '"Your App" <your_email@example.com>', to: 'recipient@example.com', subject: 'Welcome!', html: '<h1>Welcome to Our App</h1><p>We are glad to have you onboard.</p>' }; transporter.sendMail(mailOptions, function(error, info) { if (error) { return console.error('Failed to send HTML email:', error); } console.log('HTML message sent:', info.messageId); }); |
You can use inline styles or include external styles via <style> tags. Many email clients have style limitations, so always test rendering before going live.
Nodemailer also supports sending multipart emails, where both text and html parts are included. This ensures compatibility with clients that don’t support HTML.
Sending Email with Attachments
You can send files such as PDFs, images, or CSVs as email attachments. Nodemailer supports this using the attachments property:
const mailOptions = { from: '"Reports" <reports@example.com>', to: 'client@example.com', subject: 'Monthly Report', text: 'Please find the attached report.', attachments: [ { filename: 'report.pdf', path: '/path/to/report.pdf' } ] }; transporter.sendMail(mailOptions, function(error, info) { if (error) { return console.error('Sending with attachment failed:', error); } console.log('Email with attachment sent:', info.messageId); }); |
You can also attach files from a URL or use base64-encoded content:
attachments: [ { filename: 'image.jpg', content: new Buffer(base64string, 'base64'), contentType: 'image/jpeg' } ] |
Always validate file size and type. Some mailbox providers and servers limit the total size of attachments (typically to 10MB).
Handling Errors and Failures
When sending emails with SMTP, errors can happen for many reasons, like wrong login credentials, incorrect host/port, blocked connections, invalid recipient addresses, blacklisted IP, or temporary server issues.
You must handle these errors clearly and log them. Nodemailer provides a callback with error and info parameters.
transporter.sendMail(mailOptions, function(error, info) { if (error) { console.error('Send failed:', error.message); // Optional: retry logic or alert system } else { console.log('Email sent:', info.response); } }); |
You can always use try/catch when using async/await:
try { const info = await transporter.sendMail(mailOptions); console.log('Email sent:', info.messageId); } catch (error) { console.error('Send failed:', error.message); } |
Log the full error stack in development, but avoid printing it in production logs to prevent exposing sensitive data. You can also monitor the SMTP server status using transporter.verify() before sending to eliminate silent failures due to incorrect configurations.
How to Send Email with Email API in Node.js
Using an Email API can save time, improve deliverability, and give you more control over your sending process.
When to Use HTTP Email APIs
HTTP Email APIs allow you to send emails by calling an external service over HTTPS. This method does not require you to connect to a mail server manually, as with SMTP. Instead, your Node.js app sends an HTTP POST request with the email content, and the service handles the delivery.
You should use an Email API if:
- You need to send many emails quickly
- You want real-time delivery tracking
- You want to reduce server configuration work
- You need features like tags, templates, and analytics
Email APIs are reliable and relatively easy to integrate. Most modern services, including UniOne, provide detailed documentation and usage examples for sending messages.
Preparing Your UniOne API Token
To use UniOne's API, you need an API key. Follow these steps:
- Sign in to your UniOne account.
- Go to the Account – Security section.
- Create a new token or copy an existing one.
- Save it securely. You will use this token as part of the authorization header in your HTTP request.
Do not expose your token in the browser or share it publicly. It gives full access to your email-sending features. For security, always use https when sending your request and keep your token in an environment variable in your Node.js app:
require('dotenv').config(); const API_KEY = process.env.UNIONE_API_KEY; |
Sending Email via UniOne API
The UniOne API provides detailed documentation and an interactive web-based testing tool.
To send an email using UniOne's API, make a POST request to the /email/send endpoint. You can use any HTTP client, like axios or Node.js built-in https module. The body of the request must be in JSON format.
Install axios:
npm install axios |
Send a basic message:
const axios = require('axios'); const API_KEY = process.env.UNIONE_API_KEY; const payload = { message: { from_email: "sender@example.com", to: [{ email: "recipient@example.com" }], subject: "Hello from UniOne API", body: { html: "<p>This is a test email using UniOne's API.</p>", text: "This is a test email using UniOne's API." } } }; axios.post('https://eu1.unione.io/v1/email/send', payload, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' } }).then(response => { console.log("Email sent:", response.data); }).catch(error => { console.error("Failed to send:", error.response?.data || error.message); }); |
Where:
- from_email must be a verified sender.
- to is a list of recipients.
- Either html or text part is required.
- The token is sent using the Authorization header.
For more info, see UniOne’s email API service for developers.
Sending Emails with Dynamic Content or Templates
UniOne supports dynamic templates. This is helpful when sending many similar emails with small personalized changes, like names or order IDs. In your template, you can use placeholders like {{name}}, {{order_id}}, etc.
To use a template:
- Create and save the template in your UniOne dashboard.
- Get the template ID.
- Use the template_engine and template_id fields in your API request.
- Pass global_substitutions or recipient_substitutions to fill in the placeholders.
Example:
const payload = { message: { from_email: "noreply@example.com", to: [{ email: "client@example.com" }], subject: "Order Confirmation", template_engine: "simple", template_id: "template_12345", global_substitutions: { "{{name}}": "Anna", "{{order_id}}": "XK1234" } } }; |
You can also combine templates with variables in the message body if needed.
Attaching Files and Images via API
To attach files to an email via UniOne’s API, use the attachments field in the message object with these properties:
- a type (like application/pdf),
- a name (the filename),
- and a content field with a base64-encoded string.
Example with a PDF attachment is shown below:
const fs = require('fs'); const file = fs.readFileSync('./invoice.pdf'); const base64 = file.toString('base64'); const payload = { message: { from_email: "billing@example.com", to: [{ email: "user@example.com" }], subject: "Your Invoice", body: { text: "Please find your invoice attached." }, attachments: [{ type: "application/pdf", name: "invoice.pdf", content: base64 }] } }; |
Note that we do not recommend attaching large files. This makes sending much slower, and most email services limit total message size anyway. Instead, use links to cloud storage when needed.
UniOne also supports inline images using inline_attachments with a content ID you can reference in the HTML code.
Handling API Errors and Response Codes
When using the API, you must handle errors correctly. These are the common reasons to failure:
- Invalid or missing API token,
- Sending from an unverified email,
- Too many requests in a short time,
- Incorrect JSON structure,
- Missing required fields.
UniOne returns error details in the response. You should always check the response status code and body:
axios.post(url, payload, config) .then(res => { console.log("Email sent:", res.data); }) .catch(err => { if (err.response) { console.error("API error:", err.response.status, err.response.data); } else { console.error("Network error:", err.message); } }); |
Response status codes to expect are:
- 200 OK: request successful
- 400 Bad Request: request is invalid (e.g., bad JSON)
- 401 Unauthorized: missing or invalid token
- 429 Too Many Requests: rate limit exceeded
- 500 Server Error: temporary issue on UniOne side
You can retry temporary errors after a short delay. For permanent errors, first fix the request structure or data.
Sending Email to Multiple Recipients and Personalization
Using ‘To’, ‘CC’, ‘BCC’ in Node.js
For each email, you can specify one or more recipients. In Node.js, when sending emails with SMTP (using Nodemailer) or with an email API (like UniOne), you can use the following header fields:
- To: the main recipients of the email.
- CC (carbon copy): recipients who should receive a copy, visible to others.
- BCC (blind carbon copy): recipients who should receive a hidden copy.
Each field can contain either a single email or a list of emails.
Example using Nodemailer:
const mailOptions = { from: 'you@example.com', to: ['user1@example.com', 'user2@example.com'], cc: 'manager@example.com', bcc: ['auditor@example.com', 'admin@example.com'], subject: 'Monthly Report', text: 'Here is the report.' }; |
With UniOne API, use the to, cc, and bcc keys inside the message object:
const payload = { message: { from_email: 'you@example.com', to: [{ email: 'user1@example.com' }, { email: 'user2@example.com' }], cc: [{ email: 'manager@example.com' }], bcc: [{ email: 'auditor@example.com' }], subject: 'Monthly Report', body: { text: 'Here is the report.' } } }; |
Follow these useful tips:
- Always make sure email addresses are valid
- Use bcc for large recipient lists to protect user privacy
- Be careful not to mix up to, cc, and bcc - especially in automated systems
Personalizing Messages with Variables
Personalization means making each email more relevant by changing its content for each recipient. A common use case is to insert a person’s name or order number into the message.
There are two ways to personalize messages:
- Manual string replacement
- Template substitution with variables
- Manual substitution (Nodemailer):
You can generate each message for every recipient:
const users = [ { name: 'Anna', email: 'anna@example.com' }, { name: 'John', email: 'john@example.com' } ]; for (const user of users) { const mailOptions = { from: 'support@example.com', to: user.email, subject: `Hello, ${user.name}!`, text: `Dear ${user.name}, your profile has been updated.` }; transporter.sendMail(mailOptions); } |
- API-based substitution (UniOne):
You can send one request with substitutions for each recipient:
const payload = { message: { from_email: 'support@example.com', subject: 'Your Order Info', body: { html: '<p>Hello, {{name}}. Your order number is {{order_id}}.</p>', text: 'Hello, {{name}}. Your order number is {{order_id}}.' }, to: [ { email: 'anna@example.com', substitutions: { '{{name}}': 'Anna', '{{order_id}}': 'A123' } }, { email: 'john@example.com', substitutions: { '{{name}}': 'John', '{{order_id}}': 'B456' } } ] } }; |
Each recipient gets a message with different values inserted. This is much faster than generating every email separately in your node.js code and helps keep your email-sending system efficient. Make sure all variables used in the body match the keys in the substitutions object.
Bulk Email Sending with Node.js
When using SMTP to send many emails, you should not try to send all messages at once. SMTP servers usually have rate limits. If you send too many messages too fast, the server can block your IP or delay delivery.
Sending in Batches with Nodemailer
A safer approach is to send emails in batches. This means sending a few messages at a time, with small delays between them.
Example batch sending logic:
const recipients = [/* list of email addresses */]; const batchSize = 10; const delay = 2000; // 2 seconds between batches async function sendBatchEmails(transporter, recipients) { for (let i = 0; i < recipients.length; i += batchSize) { const batch = recipients.slice(i, i + batchSize); for (const email of batch) { const mailOptions = { from: 'noreply@example.com', to: email, subject: 'Newsletter', text: 'This is a bulk email test.' }; try { await transporter.sendMail(mailOptions); console.log('Email sent to:', email); } catch (err) { console.error('Failed to send to', email, err.message); } } await new Promise(resolve => setTimeout(resolve, delay)); } } |
This method works well for most transactional or alerting emails. It’s one of the easiest ways to send email node.js developers often start with. For higher volumes, consider using a message queue system like RabbitMQ, Bull, or AWS SQS to schedule and control delivery.
Using UniOne API for Bulk Messaging
UniOne provides full support for sending emails to multiple recipients in one API request. This is the best method for bulk delivery, especially when you personalize messages. The to field in UniOne's API accepts an array of recipients. You can include up to 1000 recipients in one call, depending on your plan. Each recipient can have its own substitution values. This allows you to send personalized emails without making separate API calls.
Example:
const payload = { message: { from_email: 'news@example.com', subject: 'Your Daily Update', body: { text: 'Hi {{name}}, here is your update.', html: '<p>Hi {{name}},</p><p>Here is your update.</p>' }, to: [ { email: 'anna@example.com', substitutions: { '{{name}}': 'Anna' } }, { email: 'john@example.com', substitutions: { '{{name}}': 'John' } } ] } }; |
Send the request once, and each user gets a unique email. This approach reduces API calls, speeds up delivery, and supports per-user personalization. You can read more on UniOne's email API service for developers documentation pages. For very large campaigns, UniOne supports asynchronous sending with job tracking, so you don’t need to manage retries manually.
Throttling and Queue Management
When sending a large number of emails, it is important to control the rate of sending. This is called throttling. It prevents overloading your provider and helps avoid getting blocked by spam filters.
Throttling is a control mechanism that helps your email system avoid being blocked or slowed down by the server. In practice, it means you don’t send too many emails in a short period of time. Instead, you send them gradually - spreading requests over time. This can include adding pauses between individual messages or batches and retrying later when the server is under heavy load.
This matters because most SMTP servers enforce limits. For example, they may allow only 100 emails every 10 minutes. If you exceed this rate, the server may start rejecting your requests. With API-based sending, the same issue can appear in the form of a 429 Too Many Requests error. This response means that your app sent more requests than the system allows in a given time frame.
Advice:
- Use delays between batches (see example above).
- Add retry logic if the server responds with a temporary error.
- Use a job queue to schedule email jobs. Tools like Bull (with Redis) allow you to set rate limits and retry failed jobs automatically.
Queue with rate limit:
const Queue = require('bull'); const emailQueue = new Queue('emailQueue', { limiter: { max: 50, duration: 60000 // max 50 jobs per minute } }); |
Each job in the queue would send one email. Bull manages spacing and retries for you, and logging gives you full visibility into your node.js send email operations.
Working with Email Templates in Node.js
Using templates is a common method to generate dynamic email content. Templates let you define a fixed structure and insert custom values inside. This is useful for messages like order confirmations, password resets, and newsletters.
Working with UniOne API, you also have access to two template engines, Simple and Velocity. The latter requires a bit more coding but offers advanced features.
In Node.js, the two most common template engines are Handlebars and EJS. You may want to use those with SMTP mailing or implement complex logic not supported by Velocity.
Storing and Reusing Templates
In UniOne, you can upload templates via the dashboard and reference them using template_id in the API request.
You can browse and preview from a library of saved email templates. Each design can be used in API-based campaigns by referencing its template ID.
Each template shows its creation time, associated sender, and quick actions to edit, preview, or duplicate it for use in future campaigns.
Inline CSS Styling and Responsive Layouts
Most email clients have poor support for modern CSS. To make sure your emails look correct, you need to:
- Use inline CSS only - styles must be inside HTML tags, not in <style> blocks.
<p style="font-family: Arial; color: #333;">Hello, {{name}}</p> |
- Avoid JavaScript and external stylesheets - they are ignored or blocked.
- Use tables for layout - email clients like Outlook do not support modern layout features like flexbox or grid.
- Make it responsive with media queries - some mobile clients support them. But test carefully.
Example:
<table width="100%" cellpadding="0" cellspacing="0"> <tr> <td align="center"> <table width="600" cellpadding="20" cellspacing="0" style="border: 1px solid #ddd; background: #fff;"> <tr> <td> <h1 style="margin: 0; font-size: 24px; color: #333;">Welcome, {{name}}!</h1> <p style="font-size: 16px;">Thank you for joining. We're glad to have you.</p> </td> </tr> </table> </td> </tr> </table> |
Testing Email Functionality Before Deployment
Before you send real emails to users, you should test your email-sending code in a local environment. This helps you verify that the message structure is correct, variables are inserted properly, and no sensitive or broken content is included. To test email sending without sending real emails, use MailDev or MailHog. These tools act like fake SMTP servers. Your app sends emails to them, and they show the result in a web browser.
MailDev
MailDev is a Node.js-based tool for local email testing.
Installation:
npm install -g maildev |
Start MailDev:
maildev |
This starts:
- an SMTP server at localhost:1025,
- a web interface at http://localhost:1080.
Set up your transporter in Nodemailer:
const transporter = nodemailer.createTransport({ host: 'localhost', port: 1025, ignoreTLS: true }); |
Now every email you send will appear in the MailDev browser window.
MailHog
MailHog is another tool with similar features. It is not written in Node.js but works cross-platform.
Installation (via Docker):
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog |
Web UI: http://localhost:8025
Nodemailer setup:
const transporter = nodemailer.createTransport({ host: 'localhost', port: 1025, secure: false }); |
MailHog captures the messages and shows both raw and rendered versions. It also supports message history and SMTP logs.These tools are very helpful for early-stage testing. They help you confirm the subject, body, headers, and formatting of your messages before touching a real inbox.
Using UniOne’s Email Testing Environment
When you are ready to test email delivery with real infrastructure, but without sending messages to real people, use UniOne’s Email Testing service.
UniOne allows you to:
- Send test messages using real API or SMTP credentials
- Preview emails in a secure dashboard
- Verify variable substitution, headers, and structure
- Catch invalid payloads or syntax errors
How to use it:
- Go to the Email Testing page.
- Use a test recipient address provided by UniOne (e.g. test@unione.io).
- Send your message using your normal SMTP or API code.
- Open your UniOne account dashboard.
- Go to the Email Testing section.
- View the message details, rendered preview, and logs.
Using UniOne’s testing environment allows you to review how your email will behave before it reaches a real user. It shows exactly how dynamic content is rendered, so you can confirm that all placeholders – such as names, dates, or order numbers – are being replaced correctly. This is especially useful when you use templates with substitution variables. It also lets you verify whether attachments are included in the message and delivered in the proper format. Sometimes, emails may be sent without the intended file or with a damaged attachment, and this tool helps you detect that before anything goes live.
Another important benefit is checking your headers. These include fields like Reply-To, List-Unsubscribe, and others that play a key role in deliverability and compliance. If any of them are missing or incorrectly formatted, the test result will help you spot the problem. You can also preview how your email looks in HTML form, which is critical for appearance across different email clients. Some email platforms handle formatting differently, and what looks good in one inbox may break in another. The testing system gives you a safe space to catch those layout issues early.
Debugging and Inspecting Email Payloads
When something goes wrong in email delivery, you need to inspect:
- the request payload (for API calls),
- the SMTP response (for SMTP sends),
- and the final email content.
With SMTP (e.g., Nodemailer)
Use transporter.sendMail() and check both error and info:
transporter.sendMail(mailOptions, (error, info) => { if (error) { console.error('SMTP Error:', error.message); console.error('Full error:', error); } else { console.log('Message sent:', info.messageId); console.log('Server response:', info.response); } }); |
If an error happens, check:
- server port and credentials,
- attachment size limits,
- rejected addresses,
- connection timeouts.
Use transporter.verify() before sending to check your setup.
With UniOne API
Check the API response code and body. Example using axios:
axios.post(apiUrl, payload, config) .then(response => { console.log('Success:', response.data); }) .catch(error => { if (error.response) { console.error('API Error:', error.response.status); console.error('Details:', error.response.data); } else { console.error('Network Error:', error.message); } }); |
The response will include:
- status codes (e.g., 200 OK, 400 Bad Request),
- error messages like "Invalid recipient email",
- missing field notices.
When using the UniOne API, it's important to carefully check the response from the server after each request. This will tell you if the message was accepted or if something went wrong. If your request was successful, the API will return a 200 OK status along with a confirmation in the response body. But if something is wrong with your payload, you’ll get an error response - such as 400 Bad Request if the structure is invalid, or 401 Unauthorized if your API token is missing or incorrect. The error message will often include a clear explanation, like "Invalid recipient email" or a notice about a missing field.
To avoid these problems, always review your request payload before sending it. Make sure that all required fields are included. This means you must have at least a to address, a subject, and a valid body in either plain text, HTML, or both. These are minimum requirements. If any of them are missing, the API will reject the request. Also, check that your dynamic variables are formatted and passed correctly. For example, if you use a template with {{name}} in the body but forget to provide a value for {{name}} in the substitution list, your message will either fail or be delivered with the placeholder still visible to the user.
If you're attaching files, confirm that the content-type for each attachment is valid. This means using values like application/pdf for PDF files or image/png for PNG images. A wrong or missing type can break the attachment or make the message fail altogether. Inspect your optional fields like reply_to and cc. If these fields are included in the payload but not properly formatted - for example, missing email addresses or containing invalid characters - the API may reject the whole message. Even if these fields are optional, they must still follow correct syntax when used.
How to Handle Errors in Email-Sending Code
When using SMTP to send emails (e.g. with Nodemailer), you may receive errors from the SMTP server. These errors are identified by response codes. Each code gives a specific meaning.
Basic SMTP response codes are three-digit numbers. The first digit shows the result:
- 2xx means success.
- 4xx means a temporary problem (you can retry).
- 5xx means a permanent failure (you must fix your message or settings).
Common SMTP codes are:
Status Code |
Meaning |
250 OK |
The message was accepted for delivery. |
421 |
Service not available – Server is overloaded or restarting. Retry later. |
450 |
Mailbox unavailable – The recipient mailbox is not ready. Often temporary. |
451 |
Local error in processing – Something went wrong on the server. Retry later. |
452 |
Too many recipients / insufficient storage – Limit reached. |
500 |
Syntax error / command unrecognized – You sent a malformed request. |
501 |
Invalid address – Usually means the recipient email is wrong. |
550 |
Mailbox not found / blocked – The address does not exist or is rejecting messages. |
554 |
Message rejected – Spam or policy rejection. |
Note, however, that the text descriptions following the digital error code are not standardized, and should not be relied upon for error processing.
When using Nodemailer, the error object returned in sendMail() will include:
- code: the SMTP response code (as a string),
- response: the full SMTP server message.
Example:
transporter.sendMail(mailOptions, function(error, info) { if (error) { console.error('SMTP error code:', error.code); console.error('SMTP response:', error.response); } else { console.log('Email sent:', info.messageId); } }); |
You can use the code to decide whether to retry, to alert the user, or whether to skip the recipient. We recommend avoiding retrying if the code starts with 5.
API Error Responses and Retries
When using UniOne or any email API, you must check the HTTP response to understand if the request succeeded or failed. Most APIs follow standard HTTP status codes.
Common HTTP status codes in email APIs:
HTTP Status Code |
Meaning |
200 OK |
The request was successful. |
400 Bad Request |
Your JSON or parameters are invalid. |
401 Unauthorized |
Missing or invalid API token. |
403 Forbidden |
You tried to access a restricted action. |
404 Not Found |
The API endpoint is incorrect. |
429 Too Many Requests |
You exceeded your rate limit. Wait and try again. |
500 Internal Server Error |
A temporary issue on the provider's side. You should retry only for this code. |
When using the UniOne API or any other email API, it's critical to understand how to respond to different types of HTTP errors. Not all failures should be handled the same way. Some require retrying the request after a short delay, while others signal a permanent issue that must be fixed in the code or payload before trying again.
You should only retry a request if the problem is temporary. This includes responses like 429 Too Many Requests, which means you've hit the rate limit and need to wait before sending more. It also includes 500 Internal Server Error and similar codes like 502 Bad Gateway, 503 Service Unavailable, or 504 Gateway Timeout. These errors usually mean the server is overloaded or facing internal issues and might recover soon.
To retry correctly, you must not send the same request immediately. Instead, apply exponential backoff. This means you wait a bit longer after each failed attempt - first one second, then two seconds, then four, and so on. This gives the server time to recover and lowers the chance of repeated failures.
However, you must never retry if the error is permanent. For example, a 400 Bad Request means your JSON or parameters are wrong. Fix the request before trying again. A 401 Unauthorized or 403 Forbidden means your API token is missing or not allowed to perform that action. A 404 Not Found means the endpoint you're calling doesn't exist. Retrying these errors won't help and will only waste resources or make the problem worse.
Example retry handler:
async function sendEmailWithRetry(payload, maxAttempts = 3) { let attempt = 0; while (attempt < maxAttempts) { try { const response = await axios.post(apiUrl, payload, config); console.log('Sent:', response.data); return; } catch (err) { const code = err.response?.status; if (code >= 500 || code === 429) { attempt++; const waitTime = 1000 * 2 ** attempt; console.warn(`Retrying in ${waitTime}ms...`); await new Promise(r => setTimeout(r, waitTime)); } else { console.error('API error:', code, err.response?.data); break; } } } } |
Logging and Monitoring
For each email you send, your system should write a log entry that includes several important pieces of information. You need to record the exact time the email was sent, so you can match it to events in other systems if needed. The message ID, typically returned by the SMTP server or email API, helps you track the message through delivery pipelines. You should also log the recipient's email address and optionally the subject line, so it's easier to identify the message in logs or support requests.
More importantly, every log entry should include the delivery status. If the email was accepted, note that it was successful. If it failed, capture the reason. This means storing the full response returned by the SMTP server or the email API, including any error codes or human-readable messages. If your system retries failed sends, also track how many times each message was retried and whether the retry was successful.
Logging this data consistently allows you to see patterns, such as repeated failures to a specific domain, or sudden increases in bounces. It also helps you catch silent failures where messages don’t arrive but no obvious error was raised. With proper logging, you don’t have to guess what happened - you can go back, check the record, and take action based on facts.
UniOne’s webhook configuration window lets you track key delivery events like sent, opened, bounced, or clicked in real-time.
Conclusions
When you send email with Node.js, doing it correctly, reliably, and at scale requires careful planning and a clear understanding of the tools involved. SMTP remains a stable and well-supported method, especially if you already have access to a working mail server or need tight control over delivery configuration. At the same time, email APIs - like the one offered by UniOne - have become the more efficient choice for most modern applications. They provide faster delivery, easier personalization, built-in analytics, and better support for bulk sending.
Whether you use Nodemailer or a dedicated API, what really matters is how you build your system around it. That means handling errors properly, setting up retries with care, protecting your domain reputation, and making sure each message is tested before reaching your users. The tools exist: local testing servers, real delivery sandboxes, and powerful dashboards that show you what’s happening under the hood. But they only work well when you use them deliberately.
UniOne offers everything needed to send reliable, fast, and traceable emails. And more importantly, it helps you grow from simple test emails in development to full-scale delivery systems in production without switching platforms or rewriting your code. The key is to treat email as part of your application’s architecture - not just a background feature. Because when email fails silently or ends up in spam, the user experience suffers. And when it works well, nobody notices - which is exactly what you want.
UniOne Services Than Can Help
SMTP Service
UniOne provides a secure and reliable SMTP service that works with any email client or backend. You can use this service to send emails directly from your Node.js app using standard SMTP settings. It supports authentication, encryption (TLS/SSL), and works with Nodemailer and other SMTP-compatible tools.
This is a good choice if:
- you already use SMTP in your system,
- you want simple configuration,
- you need to switch from another SMTP provider.
To get started, you only need to know these settings:
- your UniOne SMTP login (user ID) and password (API key),
- the SMTP host: smtp.unione.io,
- and port 587 (with STARTTLS).
UniOne also provides bounce handling, email authentication with SPF, DKIM, DMARC, and sending limit protection to avoid blacklists. More information is available on the SMTP Service page.
Email API Service for Developers
The UniOne API gives you a modern and powerful way to send emails through HTTP. This method is more flexible than SMTP and allows for:
- Fast delivery,
- Dynamic content,
- Full error control,
- Built-in analytics.
The API accepts JSON-formatted requests. The API uses simple authentication with a token and returns structured responses with status codes. This makes it easy to build retry logic and handle failures.
You can learn more about the email API service for developers on the dedicated page.
Email Testing Platform
UniOne includes a built-in testing environment for safe pre-deployment checks. You can send emails to test addresses without reaching real users.
This platform helps you:
- preview how dynamic variables are rendered;
- test templates before launch;
- check how messages are displayed in different email clients;
- catch issues like broken HTML, missing headers, or blocked attachments.
It’s easy to use – just send a message to a test address from your account, then open the testing dashboard to inspect the result. The tool supports both API and SMTP-based messages. You can try it via the Email Testing section on the UniOne website.
Email Analytics and Deliverability Tools
UniOne also gives you built-in tools for tracking delivery and analyzing performance.
These tools include:
- open and click tracking, using invisible pixels and link rewriting,
- bounce tracking, to monitor and suppress failed addresses,
- unsubscribe management, including link insertion and recipient opt-out lists,
- spam complaint reports, from supported ISPs,
- tagging and metadata, to group emails and run custom reports.
You can access all analytics in the UniOne dashboard. There you’ll find delivery charts, engagement stats, and filtering by tags or time.
These tools help you improve your email campaigns and stay out of spam folders by understanding what works and what doesn’t.
FAQ
Can I send emails from localhost in Node.js?
Yes, you can use node.js to send email from localhost. It only makes sense for initial testing, and of course, you’ll need to set up a working SMTP server accessible via 127.0.0.1. You may either use a regular server or a so-called “fake SMTP” which does not actually send anything but stores emails locally for debugging.
Is Nodemailer better than using an API?
Nodemailer and email APIs are not in direct competition – they just serve different purposes. Nodemailer sends emails using the SMTP protocol, which is the traditional way of delivering email. It connects directly to a mail server, establishes a session, and sends the message through that connection. This method is reliable and well-understood, and it works with any server that supports SMTP. It’s a good choice if you already have an SMTP server, if your current system is built around traditional email delivery, or if you're integrating into older infrastructure that expects direct SMTP communication.
On the other hand, an email API is used to sends messages using HTTP requests. You prepare your message as a JSON payload and send it to the provider’s API endpoint. The provider then handles the actual message delivery. This approach is faster, more versatile, and more flexible. It’s especially useful when you need advanced features like delivery tracking, analytics, or template-based messaging. It also performs better under high load, which is important if you're sending thousands of emails per hour or running large marketing campaigns.
Choosing between these two methods depends on your project. Nodemailer makes sense when you already have everything set up around SMTP and want to keep it simple. But if you’re building a modern application that needs speed, scale, and insight into delivery performance, using a service like UniOne’s email API is often the better long-term solution. Both options are valid, but they solve different problems and fit different use cases.
What’s the best way to send email attachments?
You can send attachments with node.js using both SMTP and API methods. However, sending attachments is a questionable idea, to say the least.
Definitely avoid sending very large files (above 10Mb when encoded) as most mailbox providers put a restriction on the size of attachments. For large documents, upload to cloud storage and send a link instead.
Never send executable files, password protected archives or documents containing macros – such messages are almost guaranteed to end up in spam or get bounced. Again, use external storage and email a link.
Finally, your bulk campaigns will be painfully slow if you include attachments in your newsletter. Each recipient receives their own copy of the file, which generates huge traffic. The solution… you know.
Do I need a domain to send email in production?
Yes, you must use a real and verified domain to send production emails. This helps protect your sender reputation and improves deliverability. Using free services like Gmail or Yahoo as a sender address for automated emails is not recommended. Most professional email services require you to use a domain that you own. Follow these steps:
- Buy a domain (if you don’t have one already).
- Add SPF, DKIM, and DMARC DNS records.
- Verify your domain with your email provider (like UniOne).
- Use a consistent “from” address (e.g., newsletter@yourdomain.com).
Having a reputable domain shows mail servers that you are a trusted sender. This reduces the chance of your messages being blocked or marked as spam.