QUESTPIE
Reference

Routes API

Complete reference for route(), job(), service(), and email() factories.

route()

Define server routes using the immutable route builder. Each method returns a new frozen instance.

import { route } from "questpie";
import { z } from "zod";

export default route()
  .post()
  .schema(z.object({ title: z.string(), content: z.string() }))
  .handler(async ({ input, db, collections, session }) => {
    const post = await collections.posts.create(input);
    return { id: post.id };
  });

File Convention

Routes are discovered by file convention. The file path determines the URL:

File pathHTTP endpoint
routes/health.tsGET /api/health
routes/posts/search.tsPOST /api/posts/search
routes/admin/stats.tsGET /api/admin/stats
routes/webhooks/stripe.tsPOST /api/webhooks/stripe

Builder Methods

MethodDescription
.get()Set HTTP method to GET
.post()Set HTTP method to POST (default)
.put()Set HTTP method to PUT
.delete()Set HTTP method to DELETE
.patch()Set HTTP method to PATCH
.schema()Set Zod input validation schema
.handler()Set handler function (terminal -- returns a RouteDefinition)
.raw()Raw mode -- handler receives { request } and returns Response
.access()Access control (boolean or function)

Handler Context

The handler function receives a typed context object:

PropertyTypeDescription
inputz.infer<schema>Validated input
collectionsAppCollectionsTyped collection API
globalsAppGlobalsTyped globals API
dbDatabaseDrizzle database instance
sessionSession | nullAuth session
queueQueueClientJob queue client
emailMailerServiceEmail service
loggerLoggerPino logger
kvKVStoreKey-value store
appQuestpieAppApp instance
Custom servicesanyServices from services/

Example: GET with Query Params

export default route()
  .get()
  .schema(z.object({ page: z.coerce.number().default(1) }))
  .handler(async ({ input, collections }) => {
    const posts = await collections.posts.find({
      limit: 20,
      offset: (input.page - 1) * 20,
    });
    return posts;
  });

Access Control

export default route()
  .post()
  .access(({ session }) => session?.user?.role === "admin")
  .schema(z.object({ ... }))
  .handler(async ({ input }) => { ... });

Raw Routes

Use .raw() for handlers that need direct Request/Response control. Cannot be combined with .schema().

import { route } from "questpie";

export default route()
  .get()
  .raw()
  .handler(async ({ request, db }) => {
    const url = new URL(request.url);
    const id = url.searchParams.get("id");
    // ... custom logic
    return new Response(JSON.stringify({ ok: true }), {
      headers: { "Content-Type": "application/json" },
    });
  });

job(options)

Define background jobs processed by the queue system.

import { job } from "questpie";
import { z } from "zod";

export default job({
  name: "sendWelcomeEmail",
  schema: z.object({
    userId: z.string(),
    email: z.string().email(),
  }),
  handler: async ({ payload, email, collections }) => {
    const user = await collections.users.findById(payload.userId);
    await email.sendTemplate({
      template: "welcome",
      input: { name: user.name },
      to: payload.email,
    });
  },
  options: {
    retryLimit: 3,
    retryDelay: 5000,
  },
});

Job Options

OptionTypeDescription
namestringJob identifier
schemaZodSchemaTyped payload schema
handler(ctx) => Promise<void>Handler function
options{ retryLimit, retryDelay }Retry configuration

Job Context

Same as route handler context, but input is replaced by payload (typed to the job's Zod schema).

Publishing Jobs

From any handler context (routes, hooks, other jobs):

await queue.sendWelcomeEmail.publish({
  userId: user.id,
  email: user.email,
});

service(options)

Define reusable services that are injected into handler contexts.

services/stripe.ts
import { service } from "questpie";
import Stripe from "stripe";

export default service({
  lifecycle: "singleton",
  create: () => new Stripe(process.env.STRIPE_SECRET_KEY!),
  dispose: (stripe) => {
    // Cleanup if needed
  },
});

Service Options

OptionTypeDescription
lifecycle"singleton" | "request"singleton: one instance, request: per-request
create() => TFactory function
dispose(instance) => voidOptional cleanup function

Services are available in all handler contexts by their filename:

// In a route handler
.handler(async ({ stripe }) => {
  const session = await stripe.checkout.sessions.create({ ... });
})

email(options)

Define email templates with typed input schemas.

emails/welcome.ts
import { email } from "questpie";
import { z } from "zod";

export default email({
  name: "welcome",
  schema: z.object({
    name: z.string(),
    verifyUrl: z.string().url(),
  }),
  handler: ({ input }) => ({
    subject: `Welcome, ${input.name}!`,
    html: `
      <h1>Welcome to our platform, ${input.name}!</h1>
      <p><a href="${input.verifyUrl}">Verify your email</a></p>
    `,
  }),
});

Email Options

OptionTypeDescription
namestringTemplate identifier
schemaZodSchemaTyped input schema
handler(ctx) => { subject, html }Template renderer

Sending Emails

await email.sendTemplate({
  template: "welcome",
  input: { name: "Alice", verifyUrl: "https://..." },
  to: "alice@example.com",
});

On this page