QUESTPIE
CodeEmails

Sending

ctx.email is a MailerService with three methods. sendTemplate renders a registered template and sends it, send takes a raw message, and renderTemplate stops at the HTML.

View markdown
MethodTakesReturns
sendTemplate(options)A template key and typed inputPromise<void>
send(options)A raw subject and bodyPromise<void>
renderTemplate(options)A template key and typed inputPromise<EmailResult>

ctx.email is on every handler context: hooks, routes, jobs, services, and email handlers. app.email is the same object outside a handler. Its type carries no template keys, so sendTemplate there takes a plain string and an unchecked input.

sendTemplate

Renders a registered template, then hands the result to send. Use it for anything you declared in emails/.

await ctx.email.sendTemplate({
	template: "welcome",
	input: { name: "Ada", activationUrl: "https://acme.test/activate/abc" },
	to: "ada@example.com",
});
OptionTypeNotes
templatekeyof AppEmailTemplatesRequired. The camelCased filename.
inputinferred from the schemaRequired. Parsed by the template's Zod schema.
tostring | string[]Required.
subjectstringOverrides the rendered subject.
fromstringFalls back to defaults.from.
cc, bccstring | string[]
replyTostring
localestringReaches the handler as args.locale.
ctx{ app?, db?, session? }Escape hatch outside a request or job scope.

No attachments here

sendTemplate has no attachments and no headers. To send a template with either, call renderTemplate first, then pass the result to send with your extras.

send

Takes a message you assembled yourself. This is the only method that carries attachments and custom headers.

await ctx.email.send({
	to: "ada@example.com",
	subject: "Your export is ready",
	html: "<p>Download it <a href='https://acme.test/x'>here</a>.</p>",
	attachments: [
		{ filename: "report.csv", content: csvBuffer, contentType: "text/csv" },
	],
});
OptionTypeNotes
tostring | string[]Required.
subjectstringRequired by the type.
htmlstringOne of html or text must be there.
textstringDerived from html when you omit it.
fromstringFalls back to defaults.from, then to a default.
cc, bccstring | string[]
replyTostring
attachmentsArray<{ filename; content: Buffer | string; contentType? }>
headersRecord<string, string>

Pass neither html nor text and it throws No text or html provided.

renderTemplate

Runs the handler and returns { subject, html, text? }. It never touches the adapter, so nothing is delivered.

const rendered = await ctx.email.renderTemplate({
	template: "welcome",
	input: { name: "Ada", activationUrl: "https://acme.test/activate/abc" },
});

await ctx.email.send({
	...rendered,
	to: "ada@example.com",
	attachments: [{ filename: "terms.pdf", content: pdf }],
});

It takes template, input, locale and ctx. The rest of the send options do not apply.

What gets serialized

Every adapter receives a SerializableMailOptions, which is your message with three fields settled.

FieldAfter serialization
fromYour value, else defaults.from, else noreply@example.com.
textYour value, else converted from html, else "".
htmlYour value, else "".

from is always a real address. text and html are typed string, but the one you did not supply arrives empty. A text-only message reaches the adapter with html: "".

What each call throws

ErrorCause
Template "X" not found.No template registered under that key.
A ZodErrorinput failed the template's schema.
Email handler for "X" must return a subjectThe handler returned a falsy subject.
Email handler for "X" must return htmlThe handler returned falsy html.
email template 'X' handler needs app contextNo ambient scope, and no ctx passed.
No text or html providedsend got neither body.

The first five come from renderTemplate, so they reach sendTemplate too. It renders before it sends, and nothing goes out when one of them fires.

Types

Template keys and input types are inferred from your files, so sendTemplate needs no annotations. To name a template's input type elsewhere, import the template and unwrap it.

import type { InferEmailTemplateInput } from "questpie";
import type welcome from "@/questpie/server/emails/welcome";

type WelcomeInput = InferEmailTemplateInput<typeof welcome>;
// { name: string; activationUrl: string }

The rest come from questpie/mailer, and from the root questpie export as well.

TypeWhat it is
EmailTemplateDefinitionThe { name, schema, handler } shape.
EmailResult{ subject, html, text? }.
EmailHandlerArgsAppContext & { input, locale? }.
MailOptionsThe argument to send.
SerializableMailOptionsWhat every adapter receives.
MailerConfigThe email block in your runtime config.
InferEmailTemplateInputPulls the input type off a template.
EmailTemplateNamesThe keys of a templates record.
GetEmailTemplateIndexes a templates record by key.

Codegen also emits AppEmailTemplates, the record of every template in the app. With no emails/ files it is Record<string, never>, which is why sendTemplate has nothing to autocomplete in a fresh project.

On this page