Node.js (18+)
fetch is available globally from Node 18. No polyfill needed.
import { MaildenoClient } from "maildeno"
const client = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY! })
Node.js (< 18)
npm install node-fetch
import fetch from "node-fetch"
;(globalThis as any).fetch = fetch
import { MaildenoClient } from "maildeno"
Next.js — App Router (server component or route handler)
// app/api/email/route.ts
import { MaildenoClient } from "maildeno"
const client = new MaildenoClient({
apiKey: process.env.MAILDENO_API_KEY!,
})
export async function POST(req: Request) {
const { templateId, name, plan } = await req.json()
const html = await client.renderHtml(templateId, {
merge_tags: { text: { name } },
context: { plan },
})
return Response.json({ html })
}
| Instantiate the client at module level (outside the handler) so it is reused across requests within the same Edge / serverless instance. |
Next.js — Pages Router (API route)
// pages/api/render-email.ts
import type { NextApiRequest, NextApiResponse } from "next"
import { MaildenoClient, MaildenoError } from "maildeno"
const maildeno = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY! })
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { templateId, name, plan } = req.body
try {
const html = await maildeno.renderHtml(templateId, {
merge_tags: { text: { name } },
context: { plan },
})
res.json({ html })
} catch (err) {
if (err instanceof MaildenoError) {
return res.status(err.status || 500).json({ error: err.code, message: err.message })
}
res.status(500).json({ error: "INTERNAL_ERROR" })
}
}
Express
// server.ts
import express from "express"
import { MaildenoClient, MaildenoError } from "maildeno"
const app = express()
app.use(express.json())
const maildeno = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY! })
app.post("/api/render-email", async (req, res) => {
const { templateId, name, plan } = req.body
try {
const html = await maildeno.renderHtml(templateId, {
merge_tags: { text: { name } },
context: { plan },
})
res.json({ html })
} catch (err) {
if (err instanceof MaildenoError) {
return res.status(err.status || 500).json({
error: err.code,
message: err.message,
})
}
res.status(500).json({ error: "INTERNAL_ERROR" })
}
})
app.listen(3300, () => console.log("API listening on :3300"))
Nuxt 3 (server route)
// server/api/render.post.ts
import { MaildenoClient } from "maildeno"
export default defineEventHandler(async (event) => {
const { templateId, dynamicData } = await readBody(event)
const config = useRuntimeConfig()
const client = new MaildenoClient({ apiKey: config.maildenoApiKey })
return client.render({ templateId, target: "html", dynamicData })
})
Cloudflare Workers
// worker.ts
import { MaildenoClient } from "maildeno"
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { templateId, name } = await request.json() as any
const client = new MaildenoClient({ apiKey: env.MAILDENO_API_KEY })
const html = await client.renderHtml(templateId, {
merge_tags: { text: { name } },
})
return new Response(html, { headers: { "Content-Type": "text/html" } })
},
}
Store MAILDENO_API_KEY as a Cloudflare Workers secret.
Frontend / browser usage
| Never expose your API key in browser code. |
For browser-initiated email renders, proxy through a server endpoint:
// In your browser code — calls your own backend, which holds the key
const response = await fetch("/api/render-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ templateId: "...", name: "Noruwa" }),
})
const { html } = await response.json()
Or call the Maildeno REST API directly with the Authorization header from a server:
// Server-side only — do NOT run this in the browser
const response = await fetch("https://api.maildeno.com/v1/sdk/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.MAILDENO_API_KEY}`,
},
body: JSON.stringify({
template_id: "c1a28520-c0ef-41e7-8348-da18fb7769d1",
target: "html",
dynamic_data: {
merge_tags: { text: { name: "Noruwa", company: "Maildeno" } },
context: { plan: "standard" },
},
}),
})
const html = await response.text()