QUESTPIE
Build Your BackendBusiness Logic

Routes

Typed server routes — define input schema, handler logic, and call from the client.

Routes are typed server-side logic exposed as endpoints. Define an input schema with Zod, write a handler that receives typed context, and call it from the client with full type safety.

On the client, you call client.routes.routeName().

Defining a Route

routes/get-active-barbers.ts
import { route } from "questpie";
import z from "zod";

export default route()
	.post()
	.schema(z.object({}))
	.handler(async ({ collections }) => {
		return await collections.barbers.find({
			where: { isActive: true },
		});
	});

Place route files in routes/. The filename becomes the route key: get-active-barbers.tsgetActiveBarbers.

Input Validation

Routes validate input with Zod:

routes/create-booking.ts
import { route } from "questpie";
import z from "zod";

export default route()
	.post()
	.schema(
		z.object({
			barberId: z.string(),
			serviceId: z.string(),
			scheduledAt: z.string().datetime(),
			customerName: z.string().min(2),
			customerEmail: z.string().email(),
			customerPhone: z.string().optional(),
			notes: z.string().optional(),
		}),
	)
	.handler(async ({ input, collections }) => {
		const service = await collections.services.findOne({
			where: { id: input.serviceId },
		});
		if (!service) throw new Error("Service not found");

		const appointment = await collections.appointments.create({
			barber: input.barberId,
			service: input.serviceId,
			scheduledAt: new Date(input.scheduledAt),
			status: "pending",
			notes: input.notes || null,
		});

		return {
			success: true,
			appointmentId: appointment.id,
			message: "Appointment booked successfully!",
		};
	});

Handler Context

Route handlers receive the full AppContext:

handler: async ({ input, collections, queue, email, db, session, blog }) => {
	// input — validated data matching the Zod schema
	// collections — typed collection API
	// queue — publish background jobs
	// email — send emails
	// db — raw database access
	// session — current auth session
	// blog — custom service from services/blog.ts
};

Calling Routes

From the client SDK

import { client } from "@/lib/client";

// Fully typed — input and return type inferred from the route definition
const result = await client.routes.createBooking({
	barberId: "abc",
	serviceId: "def",
	scheduledAt: "2025-03-15T10:00:00Z",
	customerName: "John",
	customerEmail: "john@example.com",
});

Via HTTP

Routes are accessible at flat URLs under your basePath (/api by default):

curl -X POST http://localhost:3000/api/create-booking \
  -H "Content-Type: application/json" \
  -d '{"barberId": "abc", "serviceId": "def", ...}'

Nested Routes

Organize routes in subdirectories for namespacing:

routes/
├── booking/
│   ├── create.ts          → client.routes.booking.create()
│   └── cancel.ts          → client.routes.booking.cancel()
├── get-active-barbers.ts  → client.routes.getActiveBarbers()
└── get-stats.ts           → client.routes.getStats()

Real-World Example

Computing available time slots with business logic:

routes/get-available-time-slots.ts
import { route } from "questpie";
import z from "zod";

export default route()
	.post()
	.schema(
		z.object({
			date: z.string(),
			barberId: z.string(),
			serviceId: z.string(),
		}),
	)
	.handler(async ({ input, collections }) => {
		const barber = await collections.barbers.findOne({
			where: { id: input.barberId, isActive: true },
		});
		if (!barber) throw new Error("Barber not found or inactive");

		const service = await collections.services.findOne({
			where: { id: input.serviceId, isActive: true },
		});
		if (!service) throw new Error("Service not found or inactive");

		// Get existing appointments for the day
		const startOfDay = new Date(input.date);
		startOfDay.setHours(0, 0, 0, 0);
		const endOfDay = new Date(input.date);
		endOfDay.setHours(23, 59, 59, 999);

		const existing = await collections.appointments.find({
			where: {
				barber: input.barberId,
				scheduledAt: { gte: startOfDay, lte: endOfDay },
			},
		});

		// Generate available slots (business logic)
		const slots = generateAvailableSlots(
			barber.workingHours,
			existing.docs,
			service.duration,
			input.date,
		);

		return slots;
	});

On this page