browsertrix/emails/api-server.ts
Emma Segal-Grossman 8db0e44843
Feat: New email templating system & service (#2712)
Co-authored-by: Ilya Kreymer <ikreymer@gmail.com>
2025-08-01 17:00:24 -04:00

95 lines
2.7 KiB
TypeScript

import express, { Request, Response } from "express";
import { pinoHttp } from "pino-http";
import { pino } from "pino";
import { render, pretty as makePretty } from "@react-email/components";
import * as templates from "./emails/index.js";
import z from "zod";
import React from "react";
// Define types for template structure
type TemplateModule = {
schema: z.ZodSchema<any>;
default: (props: any) => React.ReactElement;
subject: (props: any) => string;
};
type Templates = Record<string, TemplateModule>;
const log = pino({
level: process.env.LOG_LEVEL || "info",
name: "emails-api",
...(process.env.NODE_ENV === "development"
? {
transport: {
target: "pino-pretty",
},
}
: undefined),
});
const app = express();
app.use(pinoHttp({ logger: log }));
const port = process.env.PORT || process.env.LOCAL_EMAILS_PORT || 3000;
app.use(express.json());
// Health check endpoint
app.get("/health", (req, res) => {
req.log.trace({ msg: "Health check successful" });
res.status(200).json({ status: "ok" });
});
// Email template endpoint
app.post("/api/emails/:templateName", async (req: Request, res: Response) => {
try {
const { templateName } = req.params;
const templateKey = templateName as keyof typeof templates;
const { pretty = false } = req.body;
if (!templateName || !(templateKey in templates)) {
req.log.error({ msg: "Template not found", templateName });
res.status(404).json({ error: "Template not found" });
return;
}
// Type assertion to handle dynamic template access
const templateModule = (templates as Templates)[templateKey];
const { schema, default: Template, subject } = templateModule;
// Parse props with the specific template's schema
const props = schema.parse(req.body);
const [html, plainText] = await Promise.all([
pretty
? makePretty(await render(Template(props)))
: render(Template(props)),
render(Template(props), { plainText: true }),
]);
req.log.debug({
msg: "Email template rendered successfully",
templateName,
props,
});
res.send({ html, plainText, subject: subject(props) });
} catch (error) {
if (error instanceof z.ZodError) {
req.log.error({
msg: "Failed to render email template: zod validation error",
issues: error.issues,
body: req.body,
});
res.status(400).json({
error: "Invalid request data",
details: error.issues,
});
} else {
req.log.error({ msg: "Failed to render email template", error });
res.status(500).json({ error: "Failed to render email template" });
}
}
});
app.listen(port, () => {
log.info(`Email API server running at http://localhost:${port}`);
});