mailweaver
v1.0.1
Published
Library for sending emails
Readme
Mailweaver
✉️ mailweaver
━━━━━━━━━━━━━━━━━━━━━━━━━━
sendgrid v3, but:
• typed
• validated
• production-safeA reusable TypeScript library for sending emails via the SendGrid v3 API. Includes input validation against SendGrid limits, full type safety, production-grade error handling, and structured logging.
Installation
npm install mailweaverRequirements: Node.js 18+ (uses native fetch)
Quick Start
import { SendGridClient } from "mailweaver";
const client = new SendGridClient({ apiKey: process.env.SENDGRID_API_KEY! });
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Hello",
text: "Plain text body",
html: "<p>HTML body</p>",
});Setup
Create a SendGrid API key from the SendGrid dashboard and pass it to the client:
import { SendGridClient, createConsoleLogger } from "mailweaver";
// From environment variable (recommended)
const client = new SendGridClient({ apiKey: process.env.SENDGRID_API_KEY! });
// With optional config
const clientWithOptions = new SendGridClient({
apiKey: process.env.SENDGRID_API_KEY!,
baseUrl: "https://api.eu.sendgrid.com", // EU region
timeoutMs: 10000, // Request timeout
logger: createConsoleLogger({ minLevel: "info" }), // Structured logging
});Ensure your from address is a verified sender in your SendGrid account.
Test send (development)
When developing or cloning the repo, you can verify your SendGrid setup by sending a real test email:
SENDGRID_API_KEY=your_key [email protected] npm run test:send -- [email protected]Required environment variables:
| Variable | Description |
|----------|-------------|
| SENDGRID_API_KEY | Your SendGrid API key |
| SENDGRID_FROM_EMAIL | A verified sender address from your SendGrid account |
The -- passes the recipient email to the script. You can also load env vars from .env.local or similar if your tooling supports it.
Integration tests against SendGrid
This repo includes an optional integration test suite that can hit the real SendGrid API in sandbox mode:
SENDGRID_API_KEY=your_key [email protected] npm test -- tests/integration.test.tsBy default, when SENDGRID_API_KEY is not set, the integration tests are skipped and only fast unit tests run. Use this sparingly in CI to avoid rate limits.
Basic Usage
Simple send (string addresses)
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Welcome",
text: "Thanks for signing up!",
});HTML and plain text
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Order Confirmation",
text: "Your order #123 has been confirmed.",
html: "<p>Your order <strong>#123</strong> has been confirmed.</p>",
});Named sender and recipients
await client.send({
to: [{ email: "[email protected]", name: "Alice" }],
from: { email: "[email protected]", name: "Example Store" },
subject: "Your Order",
html: "<p>Hello Alice!</p>",
});Advanced Usage
CC and BCC
await client.send({
to: "[email protected]",
cc: ["[email protected]"],
bcc: [{ email: "[email protected]", name: "Archive" }],
from: "[email protected]",
subject: "Report",
text: "See attached.",
});Dynamic templates
await client.send({
to: "[email protected]",
from: "[email protected]",
templateId: "d-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
dynamicTemplateData: {
customerName: "Alice",
orderId: "12345",
items: ["Item A", "Item B"],
},
});Attachments
import { readFileSync } from "fs";
const pdf = readFileSync("invoice.pdf").toString("base64");
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Your Invoice",
text: "Please find your invoice attached.",
attachments: [
{
content: pdf,
filename: "invoice.pdf",
type: "application/pdf",
},
],
});Scheduled send
// Send 1 hour from now (max 72 hours ahead)
const sendAt = Math.floor(Date.now() / 1000) + 3600;
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Reminder",
text: "Don't forget!",
sendAt,
});Sandbox mode
Validate your request without actually sending:
await client.send({
to: "[email protected]",
from: "[email protected]",
subject: "Test",
text: "This won't be delivered",
sandboxMode: true,
});Error Handling
The library uses typed errors with error codes for programmatic handling. All errors extend EmailerError and include a code property.
Error types
| Error | Code | When |
|-------|------|------|
| ValidationError | VALIDATION_ERROR | Input violates SendGrid limits or format rules |
| ConfigurationError | CONFIGURATION_ERROR | Invalid client config (e.g. missing apiKey) |
| SendGridError | SENDGRID_API_ERROR | SendGrid API returns 4xx/5xx |
| TransportError | NETWORK_ERROR | Network failure (DNS, connection refused) |
| TimeoutError | TIMEOUT_ERROR | Request timed out |
| SerializationError | SERIALIZATION_ERROR | Request body could not be serialized to JSON |
ValidationError
Thrown before the request is sent:
import { SendGridClient, ValidationError } from "mailweaver";
try {
await client.send({
to: "invalid-email",
from: "[email protected]",
subject: "Test",
text: "Hello",
});
} catch (err) {
if (err instanceof ValidationError) {
console.error("Validation failed:", err.message, err.field);
}
}SendGridError and retries
Thrown when the SendGrid API returns an error. Use isRetryable() and getRetryAfterMs() for retry logic:
import { SendGridClient, SendGridError } from "mailweaver";
try {
await client.send(options);
} catch (err) {
if (err instanceof SendGridError) {
console.error("API error:", err.statusCode, err.errors);
if (err.isRetryable()) {
const delayMs = err.getRetryAfterMs(); // For 429: uses rate limit reset
if (delayMs) setTimeout(() => retry(), delayMs);
}
}
}isRetryable() returns true for 429, 5xx, and 408. getRetryAfterMs() returns a suggested delay for 429 when rate limit headers are present.
Rate limits
On 429 Too Many Requests, the error includes rateLimit with limit, remaining, and reset (Unix timestamp):
if (err instanceof SendGridError && err.rateLimit) {
console.log(`Limit: ${err.rateLimit.limit}, remaining: ${err.rateLimit.remaining}`);
console.log(`Resets at: ${new Date(err.rateLimit.reset * 1000)}`);
}Error serialization
All errors implement toJSON() for logging and monitoring:
catch (err) {
if (EmailerError.isEmailerError(err)) {
console.error(JSON.stringify(err.toJSON()));
}
}Logging
Pass a logger to enable structured, PII-safe logging. Logs are JSON-formatted and never include API keys, email content, or full addresses.
Built-in console logger
import { SendGridClient, createConsoleLogger } from "mailweaver";
const client = new SendGridClient({
apiKey: process.env.SENDGRID_API_KEY!,
logger: createConsoleLogger({
minLevel: "info", // "debug" | "info" | "warn" | "error"
prefix: "[emailer]",
}),
});Custom logger
Implement the Logger interface to integrate with pino, winston, or your logging infrastructure:
import type { Logger, LogContext } from "mailweaver";
const myLogger: Logger = {
debug: (msg, ctx) => log.debug(ctx, msg),
info: (msg, ctx) => log.info(ctx, msg),
warn: (msg, ctx) => log.warn(ctx, msg),
error: (msg, ctx) => log.error(ctx, msg),
child: (ctx) => myLogger.child ? myLogger.child(ctx) : myLogger,
};
const client = new SendGridClient({ apiKey: "...", logger: myLogger });What gets logged
- debug: Validation start, request start (recipient count, template usage)
- info: Send succeeded (status code, rate limit)
- warn: Validation failures
- error: Send failed, API errors, network/timeout errors
EU Region
For EU regional subusers, use the EU base URL:
const client = new SendGridClient({
apiKey: process.env.SENDGRID_API_KEY!,
baseUrl: "https://api.eu.sendgrid.com",
});Limitations
This library enforces SendGrid's documented limits:
| Constraint | Limit | |------------|-------| | Recipients (to + cc + bcc) | Max 1,000 per request | | Personalizations | Max 1,000 per request | | Total email size | Max 30MB | | Custom args | Max 10,000 bytes | | Reply-to list | Max 1,000 addresses | | Categories | Max 10, each max 255 chars | | Scheduled send | Max 72 hours in advance | | From field | ASCII only (no Unicode) |
See docs/LIMITATIONS.md for details.
API Reference
SendGridClient
const client = new SendGridClient(config: {
apiKey: string;
baseUrl?: string;
timeoutMs?: number;
logger?: Logger;
});| Config | Type | Description |
|--------|------|-------------|
| apiKey | string | SendGrid API key (required) |
| baseUrl | string | Override API base URL (e.g. EU region) |
| timeoutMs | number | Request timeout in milliseconds |
| logger | Logger | Optional structured logger |
send(options: SendEmailOptions): Promise
Sends an email. Returns { statusCode, headers, rateLimit? } on success.
Error classes and codes
ErrorCode– Constants:VALIDATION_ERROR,CONFIGURATION_ERROR,SENDGRID_API_ERROR,NETWORK_ERROR,TIMEOUT_ERROR,SERIALIZATION_ERROR,UNKNOWN_ERROREmailerError– Base class; useisEmailerError()andtoJSON()ValidationError– Pre-send validation failuresConfigurationError– Invalid client configSendGridError– API errors;isRetryable(),getRetryAfterMs()TransportError– Network failuresTimeoutError– Request timeoutSerializationError– JSON serialization failure
Logger utilities
createConsoleLogger(options?)– JSON logger for consolenoopLogger– No-op logger (default when none provided)redactEmail(email)– Redact email for safe loggingcreateRequestId()– Generate request correlation ID
Types
SendEmailOptions– All options for a single sendEmailAddress–{ email: string; name?: string }Attachment–{ content: string; filename: string; type?: string; disposition?: "inline" | "attachment"; content_id?: string }SendResponse–{ statusCode: number; headers: Record<string, string>; rateLimit?: RateLimitInfo }Logger–{ debug, info, warn, error, child? }
Show some love 💙
If this library saves you time or helps you ship more reliable email flows:
- Star the repo on GitHub:
DevboiDesigns/mailweaver - Share feedback or ideas via issues – real-world use cases help shape the API
- Tell a friend or teammate who is tired of hand-rolling SendGrid calls
Every star and suggestion genuinely helps keep this project healthy and evolving.
License
ISC
