QUESTPIE

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/services";
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 servicesAppContext extensionsServices 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/services";
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/services";
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.findOne({
			where: { id: 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,
});

Built-In Resource Routes

QUESTPIE's core module also registers collection and global resource routes under the API base path configured in createFetchHandler().

MethodPathPurpose
GET/:collectionFind collection records
GET/:collection/:idFind one collection record
POST/:collectionCreate a collection record
PATCH/:collection/:idUpdate a collection record
DELETE/:collection/:idDelete a collection record
GET/:collection/:id/versionsRead record version history
POST/:collection/:id/revertRevert a record to a previous version
POST/:collection/:id/transitionTransition a record workflow stage
GET/globals/:nameRead a global
PATCH/globals/:nameUpdate a global
GET/globals/:name/versionsRead global version history
POST/globals/:name/revertRevert a global to a previous version
POST/globals/:name/transitionTransition a global workflow stage

Transition bodies use the same shape for collections and globals:

type TransitionBody = {
	stage: string;
	scheduledAt?: string;
};

Use an ISO datetime for scheduledAt when scheduling a future transition. Query parameters such as locale, localeFallback, and stage are parsed on routes that read or write localized/workflow data.


service(options)

Define reusable services that are injected into handler contexts.

services/stripe.ts
import { service } from "questpie/services";
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/services";
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