Skip to content

Send email with Express

Send transactional email from Express through PostStack.

Express is the long-standing default for Node HTTP servers. The PostStack TypeScript SDK works without any adapter — instantiate once at module scope, plug into a normal Express handler. For high-throughput services, pair Express with a queue (BullMQ or Bee-Queue) so the request response is fast and the actual send happens out-of-band.

Quickstart#

Before you start

You need an API key (sk_live_…, or sk_test_… to try it without delivering anything) and a sending domain you have verified in PostStack. See Authentication and Domains.

  1. Install the SDK

    bash
    npm install @poststack.dev/sdk express
    npm install -D @types/express   # TypeScript types for Express
  2. Initialize the client

    typescript
    import { PostStack } from '@poststack.dev/sdk';
    import express from 'express';
    
    const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
    const app = express();
    app.use(express.json());
  3. Send an email

    typescript
    import { PostStack, PostStackError } from '@poststack.dev/sdk';
    import express from 'express';
    
    const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
    const app = express();
    app.use(express.json());
    
    app.post('/send', async (req, res) => {
      const { to, subject, html } = req.body;
    
      try {
        const { id } = await poststack.emails.send({
          from: 'hello@yourdomain.com',
          to: Array.isArray(to) ? to : [to],
          subject,
          html,
        });
        res.status(202).json({ id });
      } catch (err) {
        if (err instanceof PostStackError) {
          res.status(err.statusCode).json({ error: err.message });
          return;
        }
        throw err; // Express 5 forwards it to the error middleware; on Express 4 use next(err)
      }
    });
    
    app.listen(3000, () => console.log('Server running on port 3000'));
  4. Handle errors

    typescript
    import { PostStack, PostStackError } from '@poststack.dev/sdk';
    import express, { type Request, type Response, type NextFunction } from 'express';
    
    const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
    const app = express();
    app.use(express.json());
    
    app.post('/send', async (req, res, next) => {
      try {
        const { id } = await poststack.emails.send({
          from: 'hello@yourdomain.com',
          to: [req.body.to],
          subject: req.body.subject,
          html: req.body.html,
        });
        res.json({ id });
      } catch (err) {
        next(err);
      }
    });
    
    // Central error middleware — always include the catch-all (err, req, res, next)
    // so PostStackError can be classified once instead of in every handler.
    app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
      if (err instanceof PostStackError) {
        console.error('PostStack error', {
          status: err.statusCode,
          requestId: err.requestId,
          message: err.message,
        });
        res.status(err.statusCode).json({ error: err.message });
        return;
      }
      console.error('Unhandled error', err);
      res.status(500).json({ error: 'Internal Server Error' });
    });
    
    app.listen(3000);

Framework integrations#

BullMQ background queue

Offload sends into a BullMQ queue. The HTTP handler queue.add("send-email", payload) and returns immediately; a worker process pulls jobs, calls the SDK, and persists results. BullMQ handles exponential backoff, retries, and dead-letter queues out of the box.

Express middleware for auth

Build a middleware that verifies your application’s session/JWT before letting any send go through. PostStack’s API key authorises the application; your middleware authorises the user.

Server-sent events for send status

Pair the send with a webhook subscription, push received events onto an EventEmitter, and stream them to the client via SSE. The user gets live "delivered" → "opened" → "clicked" updates without polling.

Connect-style frameworks

The example handler works unchanged in Connect, Polka, and Tinyhttp — all use the same (req, res, next) signature. Same SDK, same setup, smaller framework footprint.

Common pitfalls#

Missing async error middleware

Express 4 does not catch errors thrown from async handlers automatically. Either upgrade to Express 5 (which does), use next(err) explicitly, or wrap handlers with an asyncHandler helper.

Returning before await resolves

res.json({ id }) inside a .then() callback returns synchronously while the promise is still pending. Use async/await consistently so the response only fires after the SDK call resolves.

Reading the key before dotenv has loaded

new PostStack(process.env.POSTSTACK_API_KEY!) at module scope runs at import time. If you load .env with dotenv, import dotenv/config first (or start Node with --env-file=.env), otherwise the client is built with undefined and every send fails with 401.

FAQ#

Should I use the SDK or raw fetch?

The SDK is preferred — it provides typed inputs/outputs, a structured PostStackError, automatic retries for 408/429/5xx, and an idempotency key on every send so a retry never delivers twice. Raw fetch works for one-off scripts but you would have to rebuild those.

How do I send in the background?

Push the send onto a BullMQ queue (or similar) and process it in a worker. The HTTP request returns immediately and the actual send happens out-of-band.

Does this work with Express 5?

Yes — Express 5 added native async error propagation, which makes the error middleware path even cleaner. The SDK code is unchanged between Express 4 and 5.

Reference#

The docs this guide builds on:

  • SDKs — every official client, its retries and idempotency keys
  • SMTP relay — hosts, ports, TLS modes and the X-PostStack-* headers
  • Emails API — every send field, attachments, scheduling and batch sends
  • Templates — stored templates with variables
  • Webhooks — event types, payloads and signature verification
  • Suppressions — what happens to an address after a hard bounce or complaint

Ready to send from Express?

Create a free account and an API key — no card needed.

Get started free

Next steps

Was this page helpful?