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
npm install @poststack.dev/sdk express2. Initialize the client
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
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.
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.