Skip to content

Send emails with Express

Learn how to send transactional emails using PostStack and Express.

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, BeeQueue, or Sidekick) so the request response is fast and the actual send happens out-of-band.

1. Install the SDK

bash
npm install @poststack.dev/sdk 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.json({ id });
  } catch (err) {
    if (err instanceof PostStackError) {
      return res.status(err.statusCode).json({ error: err.message });
    }
    throw err;
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

4. Handle errors

Express idioms for error handling, retries, and structured logging when calling the PostStack API.

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,
    });
    return res.status(err.statusCode).json({ error: err.message });
  }
  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.

  • Reinstantiating the client per handler

    Build `new PostStack(...)` once at module scope and reuse it. Keeping it warm preserves the connection pool across requests.

FAQ

Should I use the SDK or raw fetch?

The SDK is preferred — it provides typed inputs/outputs, structured error classes, and automatic retry headers. Raw fetch works for one-off scripts but is more verbose for production code.

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.

Related guides

Ready to send emails with Express?

Create a free account and get your API key in under a minute.