Build Your BackendBusiness Logic
Emails
Define email templates with Zod-validated input and send them from any handler.
Email templates are defined in emails/ and discovered by codegen. Each template has a Zod input schema and a handler that returns { subject, html }.
Defining an Email Template
emails/appointment-confirmation.ts
import { email } from "questpie";
import { z } from "zod";
export default email({
name: "appointment-confirmation",
schema: z.object({
customerName: z.string(),
appointmentId: z.string(),
barberName: z.string(),
serviceName: z.string(),
scheduledAt: z.string(),
}),
handler: ({ input }) => ({
subject: "Appointment Confirmed",
html: `
<h1>Appointment Confirmed</h1>
<p>Hi ${input.customerName}, your appointment is confirmed!</p>
<p><strong>Service:</strong> ${input.serviceName}</p>
<p><strong>Barber:</strong> ${input.barberName}</p>
<p><strong>Date:</strong> ${input.scheduledAt}</p>
<p>Booking ID: #${input.appointmentId.slice(0, 8).toUpperCase()}</p>
`,
}),
});The filename becomes the template key: appointment-confirmation.ts → email.sendTemplate({ template: "appointmentConfirmation", ... }).
Sending Emails
Use email.sendTemplate() from any handler:
routes/confirm-appointment.ts
import { route } from "questpie";
import { z } from "zod";
export default route()
.post()
.schema(z.object({ appointmentId: z.string() }))
.handler(async ({ input, collections, email }) => {
const appointment = await collections.appointments.findOne({
where: { id: input.appointmentId },
with: { barber: true, service: true },
});
await email.sendTemplate({
template: "appointmentConfirmation",
to: appointment.customerEmail,
input: {
customerName: appointment.customerName,
appointmentId: appointment.id,
barberName: appointment.barber.name,
serviceName: appointment.service.name,
scheduledAt: appointment.scheduledAt.toISOString(),
},
});
return { success: true };
});Handler Context
Email handlers receive the full AppContext plus validated input:
export default email({
name: "weekly-digest",
schema: z.object({ userId: z.string() }),
handler: async ({ input, collections, db }) => {
// Full AppContext available — fetch data, call services
const user = await collections.users.findOne({
where: { id: input.userId },
});
const recentPosts = await collections.posts.find({
where: { createdAt: { gte: oneWeekAgo() } },
limit: 5,
});
return {
subject: `Weekly digest for ${user.name}`,
html: renderDigestHtml(user, recentPosts.docs),
text: renderDigestText(user, recentPosts.docs), // optional plain text
};
},
});Email Result
The handler must return:
| Property | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | Email subject line |
html | string | Yes | HTML body |
text | string | No | Plain text fallback |
Sending from Hooks and Jobs
In a collection hook
.hooks({
afterChange: async ({ data, operation, email }) => {
if (operation === "create") {
await email.sendTemplate({
template: "appointmentConfirmation",
to: data.customerEmail,
input: { ... },
});
}
},
})In a background job
export default job({
handler: async ({ input, email }) => {
await email.sendTemplate({
template: "weeklyDigest",
to: input.email,
input: { userId: input.userId },
});
},
});Related Pages
- Email Infrastructure — Mailer adapter configuration
- Jobs — Send emails from background jobs
- Hooks — Send emails from lifecycle hooks