DEV Community

Cover image for Agnostic email sending in Node.js
Steve Lebleu
Steve Lebleu

Posted on

Agnostic email sending in Node.js

The Cliam package is back on track with a serious update about the quality, the dX, the flexibility and the security.

For those ones that are not aware of what Cliam can achieve for them (and believe me, he can), just a short trailer:

One input. One output. Any provider. Now more than ever.

And some number that matters

10 providers. 1 interface. 0 new runtime dependencies in v3.

Pick your provider. Switch tomorrow. Your code doesn't care.

import { Cliam } from 'cliam';

Cliam.mail('user.welcome', payload); // always the same
Enter fullscreen mode Exit fullscreen mode

The last marketing post is here. Let's quickly deep dive into the v3 release logs.

New providers

Two heavy hitters join the roster.

Resend - the developer-first email API built for the modern stack. Clean, predictable, fast.

Amazon SES - enterprise-grade deliverability at AWS scale. And because we know you hate unnecessary dependencies: SES authentication (AWS Signature V4) is implemented natively using Node.js built-in crypto. No @aws-sdk. No bloat. Zero new runtime dependencies.


Stack upgraded

Cliam now runs on TypeScript 6. ESM all the way, moduleResolution: Bundler, built with tsup. No CommonJS cruft, no compatibility shims.

The toolchain got a pass too: Biome 2.x handles linting and formatting, Bun runs the test suite, and tsc --noEmit is wired in as a dedicated typecheck script so your IDE and your CI agree on what's broken.


DX improved

The API surface got cleaner.

  • inlineImages is gone. It was never doing what you thought it was. Use content attachments like everyone else.
  • sendinblue is also gone. It was Brevo. It's still Brevo. Update your config: provider: 'brevo'.
  • Template IDs now live exclusively on the transporter configuration, not scattered across options. One place, one source of truth.
  • Theme colors are now injected as named Handlebars variables (primaryColor, secondaryColor, …) instead of post-render hex string replacement. Predictable. Debuggable. Actually makes sense.

Security improved

56 vulnerabilities. Including criticals. Now 0.

The previous architecture routed all providers - including HTTP API ones - through nodemailer, dragging along its entire dependency tree. That tree was old, wide, and full of known CVEs.

v3 cuts it at the root. HTTP API providers (Brevo, Mailersend, Mailgun, Mailjet, Mandrill, Postmark, Resend, Sendgrid, SES, Sparkpost) now talk to their APIs through our own thin HttpClient wrapper built on ky. Nodemailer stays for SMTP - but only for SMTP, pinned to its latest maintained release (v8), with a clean bill of health.

Less surface. Less trust. Less exposure.

The rest of the security picture:

  • AWS SES requests are signed per-request using a proper Signature V4 implementation: timestamp, payload hash, HMAC chain, the works. No pre-shared static tokens flying around.
  • All provider API key formats are validated at configuration time with provider-specific Joi regexes. You know immediately if something is wrong, not when your first email fails at 2am.
  • Sandbox mode remains a hard gate: nothing leaves the process when it's on.

Type system cleaned up

The internal type system went through a full consistency pass.

  • Enums replaced with as const objects + derived union types - one source of truth, no parallel declarations.
  • IMail.meta is now narrowed: from and replyTo are required by the type, not just by convention.
  • Body interfaces defined per provider, ready to be wired into build() return types for full end-to-end type coverage.

Bundle size improved

Before v3, import { Cliam } from 'cliam' silently loaded every provider, all ten of them, their HTTP clients, their payload mappers, all of it. You used Brevo. You got Sparkpost too. And Mailjet. And the rest.

Now providers are tree-shakeable. Import everything at once, or load only what you actually ship:

import { Cliam } from 'cliam/core';
import 'cliam/providers/brevo'; // that's it
Enter fullscreen mode Exit fullscreen mode

Your bundler sees exactly what you use. Nothing more makes it into your artifact.


Types improved

The public API is now fully typed and fully exported. Consumers get first-class access to every interface they need:

import type { IPayload, IClientConfiguration, IAddressable, IAttachment } from 'cliam';
Enter fullscreen mode Exit fullscreen mode

No more reaching into internals. No more any patches at the call site. Your IDE knows what meta.from is. Your compiler catches the typo before you do.


Configuration improved

Two modes, your choice, no process assassination.

// Programmatic — config lives in your code
Cliam.configure({ sandbox: true, transporters: [...] });

// File-based — config lives in .cliamrc.js
Cliam.configureFromFile('/path/to/config.js');
Enter fullscreen mode Exit fullscreen mode

The critical part: bad configuration used to call process.exit(). Your app just died, silently, in the middle of startup, with a log line you may or may not have seen. That's gone. Configuration errors now throw, which means you catch them, you log them your way, you decide what happens next. Your process is yours again.


Thanks for the reading, enjoy your dev, and see you there:

GitHub logo steve-lebleu / cliam

Agnostic transactional email sending in Node.js environment

Cliam

Node Bun TypeScript

Github action workflow status GitHub Release GPL Licence

Maintainability Code Coverage

Transactional emails with a kick

Agnostic transactional email sending in Node.js environment

> Why ?

To improve and facilitate the implementation, the flexibility and the maintenance of transactional emailing tasks.

> Features

  • Agnostic transactional email sending using web API or SMTP server. One input, one output.
  • Multiple simultaneous transporters.
  • Configuration based, not implementation based: easy switch between different modes.
  • Normalized transactions events.
  • Securized payloads.
  • Customisable default templates.

> Table of contents

> Requirements

  • Node.js >= 18.19.0
  • NPM >= 10.2.3

> Getting started

Install

> npm i cliam --save
Enter fullscreen mode Exit fullscreen mode

Configure

Cliam must be configured once at application startup, before any call to mail(). Two approaches are available.

Option A - Pass a configuration object directly:

import { Cliam } from 'cliam';
import type { IClientConfiguration } from 'cliam';
Enter fullscreen mode Exit fullscreen mode

Top comments (0)