Send emails with Bun
Learn how to send transactional emails using PostStack and Bun.
PostStack itself runs on Bun, so the TypeScript SDK is well-exercised against the runtime — no Node compatibility shims and no surprise gaps. Use the same `@poststack.dev/sdk` package you would in Node; Bun resolves it natively, ships zero polyfills, and `Bun.serve` works with the SDK without any adapter layer. For local development, `bun --hot run server.ts` gives you hot reload while the SDK keeps its persistent connection pool intact.
1. Install the SDK
bun add @poststack.dev/sdk2. Initialize the client
import { PostStack } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);3. Send an email
import { PostStack, PostStackError } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const server = Bun.serve({
port: 3000,
async fetch(req) {
if (req.method === 'POST' && new URL(req.url).pathname === '/send') {
try {
const { id } = await poststack.emails.send({
from: 'hello@yourdomain.com',
to: ['user@example.com'],
subject: 'Hello from Bun!',
html: '<h1>Welcome!</h1>',
});
return Response.json({ id });
} catch (err) {
if (err instanceof PostStackError) {
return Response.json({ error: err.message }, { status: err.statusCode });
}
throw err;
}
}
return new Response('Not found', { status: 404 });
},
});
console.log(`Listening on ${server.url}`);4. Handle errors
Bun idioms for error handling, retries, and structured logging when calling the PostStack API.
import { PostStack, PostStackError } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
async function sendWithRetry(payload: Parameters<typeof poststack.emails.send>[0]) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await poststack.emails.send(payload);
} catch (err) {
if (err instanceof PostStackError) {
if (err.statusCode === 429) {
// Honour Retry-After (seconds)
const retryAfter = Number(err.headers?.['retry-after'] ?? 1);
await Bun.sleep(retryAfter * 1000);
continue;
}
if (err.statusCode >= 500) {
await Bun.sleep(2 ** attempt * 1000);
continue;
}
// 4xx — log and re-throw
console.error('PostStack 4xx', {
status: err.statusCode,
requestId: err.requestId,
});
}
throw err;
}
}
throw new Error('sendWithRetry exhausted');
}Framework integrations
Bun.serve native server
`Bun.serve({ fetch })` is the lightest fit. Instantiate the client at module scope, call `await poststack.emails.send(...)` inside the fetch handler, and return `Response.json({ id })`. No framework needed.
Hono on Bun
Most production Bun apps reach for Hono for routing and middleware. The SDK works identically — see the dedicated Hono guide for full request handlers, validation, and middleware patterns.
Background workers
Use Bun’s built-in worker threads (`new Worker(new URL(...))`) for fire-and-forget sends or scheduled jobs. The SDK and its connection pool can run inside the worker scope without changes.
CLI scripts and cron
For one-shot sends from a `bun run scripts/notify.ts`, instantiate the client and call `emails.send` directly. Use `Bun.serve` only when you have an HTTP boundary to handle.
Common pitfalls
Spawning a new client per request
Instantiate `new PostStack(...)` once at module scope. Each handler invocation reusing the same client benefits from connection pooling and TLS keep-alive.
Forgetting `await` in a fetch handler
If you return the promise without awaiting it, Bun closes the connection before the API call finishes. Always `await poststack.emails.send(...)` and then return the response.
Mixing Bun and Node-only deps
The PostStack SDK is pure-JS and runs identically on Bun and Node. If you pair it with a library that imports `node:` modules without the prefix, Bun’s resolver may complain — pin the SDK at the latest version to avoid this.
FAQ
Does the SDK work natively on Bun?
Yes — PostStack itself runs on Bun, so the SDK is regularly tested on every release. No polyfills or compatibility flags needed.
Can I use it with Hono?
Yes. See the dedicated Hono guide. The SDK plugs in as a normal import — no adapter required.
What about hot reload?
`bun --hot` keeps the runtime alive between file changes, so the SDK’s connection pool persists. Cold restarts re-establish the pool transparently.
Related guides
Ready to send emails with Bun?
Create a free account and get your API key in under a minute.