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#
Install the SDK
bashnpm install @poststack.dev/sdk express npm install -D @types/express # TypeScript types for ExpressInitialize the client
typescriptimport { 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());Send an email
typescriptimport { 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'));Handle errors
typescriptimport { 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.