create-questpie 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +362 -119
- package/package.json +2 -3
- package/templates/elysia/AGENTS.md +56 -0
- package/templates/elysia/CLAUDE.md +39 -0
- package/templates/elysia/Dockerfile +24 -0
- package/templates/elysia/README.md +148 -0
- package/templates/elysia/docker/init-extensions.sql +11 -0
- package/templates/elysia/docker-compose.yml +21 -0
- package/templates/elysia/env.example +16 -0
- package/templates/elysia/gitignore +6 -0
- package/templates/elysia/package.json +47 -0
- package/templates/elysia/questpie.config.ts +12 -0
- package/templates/elysia/src/index.ts +21 -0
- package/templates/elysia/src/lib/auth-client.ts +32 -0
- package/templates/elysia/src/lib/client.ts +13 -0
- package/templates/elysia/src/lib/env.ts +24 -0
- package/templates/elysia/src/lib/query-client.ts +18 -0
- package/templates/elysia/src/lib/query.ts +18 -0
- package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
- package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/elysia/src/questpie/server/app.ts +10 -0
- package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
- package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
- package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
- package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
- package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/elysia/src/questpie/server/modules.ts +8 -0
- package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
- package/templates/elysia/tsconfig.json +28 -0
- package/templates/hono/AGENTS.md +56 -0
- package/templates/hono/CLAUDE.md +39 -0
- package/templates/hono/Dockerfile +24 -0
- package/templates/hono/README.md +148 -0
- package/templates/hono/docker/init-extensions.sql +11 -0
- package/templates/hono/docker-compose.yml +21 -0
- package/templates/hono/env.example +16 -0
- package/templates/hono/gitignore +6 -0
- package/templates/hono/package.json +47 -0
- package/templates/hono/questpie.config.ts +12 -0
- package/templates/hono/src/index.ts +30 -0
- package/templates/hono/src/lib/auth-client.ts +32 -0
- package/templates/hono/src/lib/client.ts +13 -0
- package/templates/hono/src/lib/env.ts +24 -0
- package/templates/hono/src/lib/query-client.ts +18 -0
- package/templates/hono/src/lib/query.ts +18 -0
- package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
- package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/hono/src/questpie/server/app.ts +10 -0
- package/templates/hono/src/questpie/server/collections/index.ts +1 -0
- package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/hono/src/questpie/server/config/auth.ts +8 -0
- package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
- package/templates/hono/src/questpie/server/globals/index.ts +1 -0
- package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/hono/src/questpie/server/modules.ts +8 -0
- package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
- package/templates/hono/tsconfig.json +28 -0
- package/templates/next/AGENTS.md +55 -0
- package/templates/next/CLAUDE.md +39 -0
- package/templates/next/Dockerfile +25 -0
- package/templates/next/README.md +148 -0
- package/templates/next/components.json +22 -0
- package/templates/next/docker/init-extensions.sql +11 -0
- package/templates/next/docker-compose.yml +21 -0
- package/templates/next/env.example +16 -0
- package/templates/next/gitignore +10 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.ts +20 -0
- package/templates/next/package.json +54 -0
- package/templates/next/postcss.config.mjs +8 -0
- package/templates/next/public/.gitkeep +0 -0
- package/templates/next/questpie.config.ts +12 -0
- package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
- package/templates/next/src/app/admin/admin.css +4 -0
- package/templates/next/src/app/admin/layout.tsx +63 -0
- package/templates/next/src/app/api/[...all]/route.ts +24 -0
- package/templates/next/src/app/layout.tsx +24 -0
- package/templates/next/src/app/not-found.tsx +18 -0
- package/templates/next/src/app/page.tsx +74 -0
- package/templates/next/src/app/providers.tsx +11 -0
- package/templates/next/src/lib/auth-client.ts +12 -0
- package/templates/next/src/lib/client.ts +13 -0
- package/templates/next/src/lib/env.ts +24 -0
- package/templates/next/src/lib/query-client.ts +18 -0
- package/templates/next/src/lib/query.ts +18 -0
- package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/next/src/questpie/admin/admin.ts +9 -0
- package/templates/next/src/questpie/admin/modules.ts +3 -0
- package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
- package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
- package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
- package/templates/next/src/questpie/server/.generated/index.ts +139 -0
- package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
- package/templates/next/src/questpie/server/app.ts +10 -0
- package/templates/next/src/questpie/server/collections/index.ts +1 -0
- package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
- package/templates/next/src/questpie/server/config/admin.ts +80 -0
- package/templates/next/src/questpie/server/config/auth.ts +8 -0
- package/templates/next/src/questpie/server/config/openapi.ts +10 -0
- package/templates/next/src/questpie/server/globals/index.ts +1 -0
- package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
- package/templates/next/src/questpie/server/modules.ts +9 -0
- package/templates/next/src/questpie/server/questpie.config.ts +21 -0
- package/templates/next/src/styles.css +125 -0
- package/templates/next/tsconfig.json +37 -0
- package/templates/tanstack-start/AGENTS.md +35 -607
- package/templates/tanstack-start/CLAUDE.md +26 -134
- package/templates/tanstack-start/README.md +13 -1
- package/templates/tanstack-start/docker/init-extensions.sql +11 -0
- package/templates/tanstack-start/docker-compose.yml +1 -0
- package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
- package/templates/tanstack-start/src/lib/client.ts +1 -1
- package/templates/tanstack-start/src/lib/query.ts +18 -0
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
- package/templates/tanstack-start/src/routes/__root.tsx +31 -1
- package/templates/tanstack-start/src/routes/api/$.ts +1 -1
- package/templates/tanstack-start/src/routes/index.tsx +97 -0
- package/skills/questpie/AGENTS.md +0 -2871
- package/skills/questpie/SKILL.md +0 -293
- package/skills/questpie/coverage.json +0 -213
- package/skills/questpie/references/auth.md +0 -236
- package/skills/questpie/references/business-logic.md +0 -620
- package/skills/questpie/references/codegen-plugin-api.md +0 -382
- package/skills/questpie/references/crud-api.md +0 -580
- package/skills/questpie/references/data-modeling.md +0 -509
- package/skills/questpie/references/extend.md +0 -584
- package/skills/questpie/references/field-types.md +0 -398
- package/skills/questpie/references/infrastructure-adapters.md +0 -720
- package/skills/questpie/references/mcp.md +0 -147
- package/skills/questpie/references/multi-tenancy.md +0 -363
- package/skills/questpie/references/production.md +0 -640
- package/skills/questpie/references/query-operators.md +0 -125
- package/skills/questpie/references/quickstart.md +0 -562
- package/skills/questpie/references/rules.md +0 -454
- package/skills/questpie/references/sandbox.md +0 -110
- package/skills/questpie/references/tanstack-query.md +0 -543
- package/skills/questpie/references/type-inference.md +0 -167
- package/skills/questpie/references/workflows.md +0 -155
- package/skills/questpie-admin/AGENTS.md +0 -1515
- package/skills/questpie-admin/SKILL.md +0 -443
- package/skills/questpie-admin/references/blocks.md +0 -331
- package/skills/questpie-admin/references/custom-ui.md +0 -305
- package/skills/questpie-admin/references/views.md +0 -449
|
@@ -1,620 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: questpie-core-business-logic
|
|
3
|
-
description:
|
|
4
|
-
QUESTPIE routes jobs services emails route job service email background queue scheduling Zod input validation server-side logic reusable services email templates
|
|
5
|
-
- questpie-core
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# QUESTPIE Business Logic - Routes, Jobs, Services, Emails
|
|
9
|
-
|
|
10
|
-
This skill builds on questpie-core. It covers four business-logic primitives: routes (JSON and raw HTTP), jobs (background tasks), services (reusable logic), and emails (templates).
|
|
11
|
-
|
|
12
|
-
## Routes (JSON)
|
|
13
|
-
|
|
14
|
-
JSON routes are typed server-side endpoints. Define an input schema with Zod, write a handler, and call it from the client with full type safety.
|
|
15
|
-
|
|
16
|
-
### Defining a Route
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
// routes/get-active-barbers.ts
|
|
20
|
-
import { route } from "questpie/services";
|
|
21
|
-
import z from "zod";
|
|
22
|
-
|
|
23
|
-
export default route()
|
|
24
|
-
.post()
|
|
25
|
-
.schema(z.object({}))
|
|
26
|
-
.handler(async ({ collections }) => {
|
|
27
|
-
return await collections.barbers.find({
|
|
28
|
-
where: { isActive: true },
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Place files in `routes/`. The filename becomes the route key: `get-active-barbers.ts` maps to `getActiveBarbers`. Files **must** use `export default`.
|
|
34
|
-
|
|
35
|
-
### Input Validation
|
|
36
|
-
|
|
37
|
-
JSON routes validate input with Zod automatically:
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
// routes/create-booking.ts
|
|
41
|
-
import { route } from "questpie/services";
|
|
42
|
-
import z from "zod";
|
|
43
|
-
|
|
44
|
-
export default route()
|
|
45
|
-
.post()
|
|
46
|
-
.schema(
|
|
47
|
-
z.object({
|
|
48
|
-
barberId: z.string(),
|
|
49
|
-
serviceId: z.string(),
|
|
50
|
-
scheduledAt: z.string().datetime(),
|
|
51
|
-
customerName: z.string().min(2),
|
|
52
|
-
customerEmail: z.string().email(),
|
|
53
|
-
notes: z.string().optional(),
|
|
54
|
-
}),
|
|
55
|
-
)
|
|
56
|
-
.handler(async ({ input, collections }) => {
|
|
57
|
-
const service = await collections.services.findOne({
|
|
58
|
-
where: { id: input.serviceId },
|
|
59
|
-
});
|
|
60
|
-
if (!service) throw new Error("Service not found");
|
|
61
|
-
|
|
62
|
-
const appointment = await collections.appointments.create({
|
|
63
|
-
barber: input.barberId,
|
|
64
|
-
service: input.serviceId,
|
|
65
|
-
scheduledAt: new Date(input.scheduledAt),
|
|
66
|
-
status: "pending",
|
|
67
|
-
notes: input.notes || null,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
success: true,
|
|
72
|
-
appointmentId: appointment.id,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Handler Context
|
|
78
|
-
|
|
79
|
-
Route handlers receive the full `AppContext`:
|
|
80
|
-
|
|
81
|
-
| Property | Description |
|
|
82
|
-
| ------------- | ---------------------------------------------------------- |
|
|
83
|
-
| `input` | Validated data matching the Zod schema |
|
|
84
|
-
| `collections` | Typed collection API |
|
|
85
|
-
| `queue` | Publish background jobs |
|
|
86
|
-
| `email` | Send emails |
|
|
87
|
-
| `db` | Raw database access |
|
|
88
|
-
| `session` | Current auth session |
|
|
89
|
-
| `services` | Custom services from `services/` |
|
|
90
|
-
| _extensions_ | `appConfig({ context })` result, flat (e.g. `workspaceId`) |
|
|
91
|
-
|
|
92
|
-
Derived request context (from `appConfig({ context })`) reaches route access rules and handlers alike — destructure the keys directly. Inside any nested code, `getContext<App>()` exposes the same keys (see `references/multi-tenancy.md`).
|
|
93
|
-
|
|
94
|
-
### Calling Routes
|
|
95
|
-
|
|
96
|
-
From the client SDK:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import { client } from "@/lib/client";
|
|
100
|
-
|
|
101
|
-
const result = await client.routes.createBooking({
|
|
102
|
-
barberId: "abc",
|
|
103
|
-
serviceId: "def",
|
|
104
|
-
scheduledAt: "2025-03-15T10:00:00Z",
|
|
105
|
-
customerName: "John",
|
|
106
|
-
customerEmail: "john@example.com",
|
|
107
|
-
});
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Via HTTP: `POST /api/create-booking` with JSON body.
|
|
111
|
-
|
|
112
|
-
### Nested Routes
|
|
113
|
-
|
|
114
|
-
Organize in subdirectories for namespacing:
|
|
115
|
-
|
|
116
|
-
```text
|
|
117
|
-
routes/
|
|
118
|
-
booking/
|
|
119
|
-
create.ts --> client.routes.booking.create()
|
|
120
|
-
cancel.ts --> client.routes.booking.cancel()
|
|
121
|
-
get-active-barbers.ts --> client.routes.getActiveBarbers()
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Jobs
|
|
125
|
-
|
|
126
|
-
Jobs are background tasks that run outside the request lifecycle. Ideal for sending emails, processing data, or any work that should not block an API response.
|
|
127
|
-
|
|
128
|
-
### Defining a Job
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// jobs/send-appointment-confirmation.ts
|
|
132
|
-
import { job } from "questpie/services";
|
|
133
|
-
import z from "zod";
|
|
134
|
-
|
|
135
|
-
export default job({
|
|
136
|
-
name: "sendAppointmentConfirmation",
|
|
137
|
-
schema: z.object({
|
|
138
|
-
appointmentId: z.string(),
|
|
139
|
-
customerId: z.string(),
|
|
140
|
-
}),
|
|
141
|
-
handler: async ({ payload, email, collections }) => {
|
|
142
|
-
const customer = await collections.user.findOne({
|
|
143
|
-
where: { id: payload.customerId },
|
|
144
|
-
});
|
|
145
|
-
if (!customer) return;
|
|
146
|
-
|
|
147
|
-
const appointment = await collections.appointments.findOne({
|
|
148
|
-
where: { id: payload.appointmentId },
|
|
149
|
-
with: { barber: true, service: true },
|
|
150
|
-
});
|
|
151
|
-
if (!appointment) return;
|
|
152
|
-
|
|
153
|
-
await email.sendTemplate({
|
|
154
|
-
template: "appointmentConfirmation",
|
|
155
|
-
input: {
|
|
156
|
-
customerName: customer.name,
|
|
157
|
-
appointmentId: appointment.id,
|
|
158
|
-
barberName: appointment.barber.name,
|
|
159
|
-
serviceName: appointment.service.name,
|
|
160
|
-
scheduledAt: appointment.scheduledAt.toISOString(),
|
|
161
|
-
},
|
|
162
|
-
to: customer.email,
|
|
163
|
-
});
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Place files in `jobs/`. The filename becomes the job key: `send-appointment-confirmation.ts` maps to `sendAppointmentConfirmation`.
|
|
169
|
-
|
|
170
|
-
### Publishing Jobs
|
|
171
|
-
|
|
172
|
-
Publish from hooks, routes, or other jobs via the typed `queue` context:
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
.hooks({
|
|
176
|
-
afterChange: async ({ data, operation, queue }) => {
|
|
177
|
-
if (operation === "create") {
|
|
178
|
-
await queue.sendAppointmentConfirmation.publish({
|
|
179
|
-
appointmentId: data.id,
|
|
180
|
-
customerId: data.customer,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
})
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
The `queue` object provides full autocompletion for all jobs and their payloads.
|
|
188
|
-
|
|
189
|
-
### Recurring Jobs (Cron)
|
|
190
|
-
|
|
191
|
-
Jobs accept a job-level cron expression in `options.cron`. Schedules are registered automatically when the queue worker starts (`app.queue.listen()` calls `registerSchedules()`):
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
// jobs/cleanup-expired.ts
|
|
195
|
-
import { job } from "questpie/services";
|
|
196
|
-
import z from "zod";
|
|
197
|
-
|
|
198
|
-
export default job({
|
|
199
|
-
name: "cleanupExpired",
|
|
200
|
-
schema: z.object({}),
|
|
201
|
-
options: { cron: "0 3 * * *" }, // every day at 03:00
|
|
202
|
-
handler: async ({ collections }) => {
|
|
203
|
-
await collections.sessions.deleteMany({
|
|
204
|
-
where: { expiresAt: { lt: new Date() } },
|
|
205
|
-
});
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Programmatic scheduling from any handler: `queue.cleanupExpired.schedule({}, "0 3 * * *")` and `queue.cleanupExpired.unschedule()`.
|
|
211
|
-
|
|
212
|
-
Use job-level cron for simple recurring tasks (cleanup, digests, syncs). Reach for **workflow-level cron** (`references/workflows.md`) only when the recurring process needs steps, durable waits, or replay — a workflow is the heavier primitive.
|
|
213
|
-
|
|
214
|
-
### Job Handler Context
|
|
215
|
-
|
|
216
|
-
| Property | Description |
|
|
217
|
-
| ------------- | -------------------------------------- |
|
|
218
|
-
| `payload` | Validated data matching the Zod schema |
|
|
219
|
-
| `collections` | Typed collection API |
|
|
220
|
-
| `email` | Email service |
|
|
221
|
-
| `queue` | Publish other jobs |
|
|
222
|
-
| `db` | Database instance |
|
|
223
|
-
|
|
224
|
-
### Queue Adapter Configuration
|
|
225
|
-
|
|
226
|
-
Configure the queue adapter in your runtime config:
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
// questpie.config.ts
|
|
230
|
-
import { runtimeConfig } from "questpie/app";
|
|
231
|
-
import { pgBossAdapter } from "questpie/adapters/pg-boss";
|
|
232
|
-
|
|
233
|
-
export default runtimeConfig({
|
|
234
|
-
queue: {
|
|
235
|
-
adapter: pgBossAdapter({
|
|
236
|
-
connectionString: process.env.DATABASE_URL,
|
|
237
|
-
}),
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Raw Routes
|
|
243
|
-
|
|
244
|
-
Raw routes (`route().raw()`) give raw HTTP request/response handling for webhooks, OAuth callbacks, health checks, file downloads, and streaming. The handler receives the standard `Request` and must return a `Response`.
|
|
245
|
-
|
|
246
|
-
### Defining a Raw Route
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
// routes/health.ts
|
|
250
|
-
import { sql } from "questpie/drizzle";
|
|
251
|
-
import { route } from "questpie/services";
|
|
252
|
-
|
|
253
|
-
export default route()
|
|
254
|
-
.get()
|
|
255
|
-
.raw()
|
|
256
|
-
.access(true)
|
|
257
|
-
.handler(async ({ db }) => {
|
|
258
|
-
const healthy = await db
|
|
259
|
-
.execute(sql`SELECT 1`)
|
|
260
|
-
.then(() => true)
|
|
261
|
-
.catch(() => false);
|
|
262
|
-
return Response.json({ status: healthy ? "ok" : "degraded" });
|
|
263
|
-
});
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Place files in `routes/`. The file path maps to a flat URL under your `basePath` (`/api` by default):
|
|
267
|
-
|
|
268
|
-
```text
|
|
269
|
-
routes/
|
|
270
|
-
health.ts --> /api/health
|
|
271
|
-
webhooks/
|
|
272
|
-
stripe.ts --> /api/webhooks/stripe
|
|
273
|
-
export.ts --> /api/export
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Route Methods
|
|
277
|
-
|
|
278
|
-
Chain HTTP method calls on the builder (multiple methods = multiple calls):
|
|
279
|
-
|
|
280
|
-
```ts
|
|
281
|
-
route().post().raw().handler(...) // POST only
|
|
282
|
-
route().get().post().raw().handler(...) // GET + POST
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Supported: `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`. The built-in `/auth/*` catch-all is itself a raw route (`route().get().post().raw()` delegating to Better Auth) — raw handlers run inside `runWithContext`, so the full request context is live in any code they call.
|
|
286
|
-
|
|
287
|
-
### Route Handler Context
|
|
288
|
-
|
|
289
|
-
| Property | Type | Description |
|
|
290
|
-
| ------------- | ------------------------ | -------------------------------------------- |
|
|
291
|
-
| `request` | `Request` | Standard Web API Request |
|
|
292
|
-
| `params` | `Record<string, string>` | URL path parameters |
|
|
293
|
-
| `locale` | `string` | Current locale |
|
|
294
|
-
| `db` | `Database` | Database instance |
|
|
295
|
-
| `session` | `Session \| null` | Current auth session |
|
|
296
|
-
| `collections` | `CollectionsAPI` | Typed collection API |
|
|
297
|
-
| `queue` | `QueueClient` | Queue client |
|
|
298
|
-
| `email` | `MailerService` | Email service |
|
|
299
|
-
| `services` | | User-defined services |
|
|
300
|
-
| _extensions_ | | `appConfig({ context })` result, flat |
|
|
301
|
-
|
|
302
|
-
Route handlers must return a `Response` object.
|
|
303
|
-
|
|
304
|
-
### JSON Routes vs Raw Routes
|
|
305
|
-
|
|
306
|
-
| Aspect | JSON route | Raw route |
|
|
307
|
-
| ------------- | ------------------------------- | --------------------------------- |
|
|
308
|
-
| **Transport** | HTTP JSON (`/api/{path}`) | Raw HTTP (`/api/{path}`) |
|
|
309
|
-
| **Input** | Zod-validated, auto-parsed | Manual: `request.json()` |
|
|
310
|
-
| **Output** | Auto-serialized to JSON | Raw `Response` object |
|
|
311
|
-
| **Client** | `client.routes.name(input)` | `client.routes["name"]()` |
|
|
312
|
-
| **Use for** | Business logic, data operations | Webhooks, file uploads, streaming |
|
|
313
|
-
|
|
314
|
-
**Rule of thumb**: Use JSON routes for typed input/output with automatic validation. Use raw routes for HTTP-level control (custom headers, binary data, streams, signature verification).
|
|
315
|
-
|
|
316
|
-
### Webhook Example (Signature Verification)
|
|
317
|
-
|
|
318
|
-
Webhooks need the raw body for signature verification — exactly what `.raw()` is for:
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
// routes/webhooks/stripe.ts
|
|
322
|
-
import { route } from "questpie/services";
|
|
323
|
-
|
|
324
|
-
export default route()
|
|
325
|
-
.post()
|
|
326
|
-
.raw()
|
|
327
|
-
.access(true) // signature IS the auth — verify it yourself below
|
|
328
|
-
.handler(async ({ request, collections, queue }) => {
|
|
329
|
-
const body = await request.text();
|
|
330
|
-
const signature = request.headers.get("stripe-signature");
|
|
331
|
-
const event = verifyStripeWebhook(body, signature); // throws on bad signature
|
|
332
|
-
if (!event) return new Response("Invalid signature", { status: 401 });
|
|
333
|
-
|
|
334
|
-
await collections.webhook_events.create({
|
|
335
|
-
type: event.type,
|
|
336
|
-
payload: body,
|
|
337
|
-
});
|
|
338
|
-
await queue.processStripeEvent.publish({ eventId: event.id });
|
|
339
|
-
|
|
340
|
-
return new Response("OK", { status: 200 });
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Streamed Response Example
|
|
345
|
-
|
|
346
|
-
Raw routes can return any `Response`, including streams — CSV exports, server-sent progress, large file proxies:
|
|
347
|
-
|
|
348
|
-
```ts
|
|
349
|
-
// routes/export.ts
|
|
350
|
-
import { route } from "questpie/services";
|
|
351
|
-
|
|
352
|
-
export default route()
|
|
353
|
-
.get()
|
|
354
|
-
.raw()
|
|
355
|
-
.access(({ session }) => !!session)
|
|
356
|
-
.handler(async ({ collections }) => {
|
|
357
|
-
const { docs } = await collections.orders.find({ limit: 10_000 });
|
|
358
|
-
|
|
359
|
-
const stream = new ReadableStream({
|
|
360
|
-
start(controller) {
|
|
361
|
-
controller.enqueue("id,total,createdAt\n");
|
|
362
|
-
for (const order of docs) {
|
|
363
|
-
controller.enqueue(`${order.id},${order.total},${order.createdAt.toISOString()}\n`);
|
|
364
|
-
}
|
|
365
|
-
controller.close();
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
return new Response(stream, {
|
|
370
|
-
headers: {
|
|
371
|
-
"Content-Type": "text/csv; charset=utf-8",
|
|
372
|
-
"Content-Disposition": 'attachment; filename="orders.csv"',
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
## Services
|
|
379
|
-
|
|
380
|
-
Services are reusable units of logic injected into `AppContext` under the `services` key. Define in `services/`, and they become available in every hook, route, and job handler.
|
|
381
|
-
|
|
382
|
-
### Defining a Service
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
// services/blog.ts
|
|
386
|
-
import { service } from "questpie/services";
|
|
387
|
-
const WORDS_PER_MINUTE = 200;
|
|
388
|
-
|
|
389
|
-
function stripHtml(html: string): string {
|
|
390
|
-
return html.replace(/<[^>]*>/g, " ");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export default service({
|
|
394
|
-
lifecycle: "singleton",
|
|
395
|
-
create: () => ({
|
|
396
|
-
computeReadingTime(content: string): number {
|
|
397
|
-
const text = stripHtml(content);
|
|
398
|
-
const words = text
|
|
399
|
-
.trim()
|
|
400
|
-
.split(/\s+/)
|
|
401
|
-
.filter((w) => w.length > 0).length;
|
|
402
|
-
return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
generateSlug(title: string): string {
|
|
406
|
-
return title
|
|
407
|
-
.toLowerCase()
|
|
408
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
409
|
-
.trim()
|
|
410
|
-
.replace(/\s+/g, "-");
|
|
411
|
-
},
|
|
412
|
-
}),
|
|
413
|
-
});
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
The filename becomes the key: `services/blog.ts` maps to `ctx.services.blog`.
|
|
417
|
-
|
|
418
|
-
### Using Services
|
|
419
|
-
|
|
420
|
-
Services are available via `services` destructuring in any handler:
|
|
421
|
-
|
|
422
|
-
```ts
|
|
423
|
-
.hooks({
|
|
424
|
-
beforeChange: async ({ data, services }) => {
|
|
425
|
-
const { blog } = services;
|
|
426
|
-
if (data.content) {
|
|
427
|
-
data.readingTime = blog.computeReadingTime(data.content);
|
|
428
|
-
}
|
|
429
|
-
if (data.title) {
|
|
430
|
-
data.slug = blog.generateSlug(data.title);
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
})
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
### Lifecycle
|
|
437
|
-
|
|
438
|
-
| Lifecycle | Created | Destroyed | Use for |
|
|
439
|
-
| ------------- | ------------------- | -------------- | ---------------------------------------- |
|
|
440
|
-
| `"singleton"` | Once at app startup | App shutdown | External clients, SDKs, connection pools |
|
|
441
|
-
| `"request"` | Per request | End of request | Tenant-scoped DB, user-specific config |
|
|
442
|
-
|
|
443
|
-
### Singleton Service
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
// services/stripe.ts
|
|
447
|
-
import { service } from "questpie/services";
|
|
448
|
-
import Stripe from "stripe";
|
|
449
|
-
|
|
450
|
-
export default service({
|
|
451
|
-
lifecycle: "singleton",
|
|
452
|
-
create: () => new Stripe(process.env.STRIPE_SECRET_KEY!),
|
|
453
|
-
});
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Request-Scoped Service
|
|
457
|
-
|
|
458
|
-
```ts
|
|
459
|
-
// services/tenant-db.ts
|
|
460
|
-
import { service } from "questpie/services";
|
|
461
|
-
export default service({
|
|
462
|
-
lifecycle: "request",
|
|
463
|
-
deps: ["db", "session"] as const,
|
|
464
|
-
create: ({ db, session }) => {
|
|
465
|
-
return createScopedDb(db, session?.user?.tenantId);
|
|
466
|
-
},
|
|
467
|
-
dispose: (scopedDb) => scopedDb.release(),
|
|
468
|
-
});
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Dependencies
|
|
472
|
-
|
|
473
|
-
Services can depend on other services and infrastructure via `deps`. Use `as const` for type safety:
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
// services/analytics.ts
|
|
477
|
-
import { service } from "questpie/services";
|
|
478
|
-
export default service({
|
|
479
|
-
deps: ["db", "logger"] as const,
|
|
480
|
-
create: ({ db, logger }) => {
|
|
481
|
-
logger.info("Analytics service initialized");
|
|
482
|
-
return new AnalyticsService(db);
|
|
483
|
-
},
|
|
484
|
-
});
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
Available dependencies: `db`, `logger`, `kv`, `email`, `queue`, `storage`, `search`, `realtime`, `session`, and any user-defined services.
|
|
488
|
-
|
|
489
|
-
### Disposal
|
|
490
|
-
|
|
491
|
-
Optional `dispose` for cleanup (singleton: at shutdown, request: at end of request):
|
|
492
|
-
|
|
493
|
-
```ts
|
|
494
|
-
export default service({
|
|
495
|
-
lifecycle: "singleton",
|
|
496
|
-
create: () => createConnectionPool(),
|
|
497
|
-
dispose: async (pool) => {
|
|
498
|
-
await pool.close();
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
### API Reference
|
|
504
|
-
|
|
505
|
-
```ts
|
|
506
|
-
service({
|
|
507
|
-
lifecycle?: "singleton" | "request", // default: "singleton"
|
|
508
|
-
deps?: readonly string[], // dependencies from AppContext
|
|
509
|
-
create: (deps) => TInstance, // factory function
|
|
510
|
-
dispose?: (instance) => void | Promise<void>, // cleanup
|
|
511
|
-
})
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## Emails
|
|
515
|
-
|
|
516
|
-
Email templates are defined in `emails/` and discovered by codegen. Each template has a Zod input schema and a handler that returns `{ subject, html }`.
|
|
517
|
-
|
|
518
|
-
### Defining an Email Template
|
|
519
|
-
|
|
520
|
-
```ts
|
|
521
|
-
// emails/appointment-confirmation.ts
|
|
522
|
-
import { email } from "questpie/services";
|
|
523
|
-
import { z } from "zod";
|
|
524
|
-
|
|
525
|
-
export default email({
|
|
526
|
-
name: "appointment-confirmation",
|
|
527
|
-
schema: z.object({
|
|
528
|
-
customerName: z.string(),
|
|
529
|
-
appointmentId: z.string(),
|
|
530
|
-
barberName: z.string(),
|
|
531
|
-
serviceName: z.string(),
|
|
532
|
-
scheduledAt: z.string(),
|
|
533
|
-
}),
|
|
534
|
-
handler: ({ input }) => ({
|
|
535
|
-
subject: "Appointment Confirmed",
|
|
536
|
-
html: `
|
|
537
|
-
<h1>Appointment Confirmed</h1>
|
|
538
|
-
<p>Hi ${input.customerName}, your appointment is confirmed!</p>
|
|
539
|
-
<p><strong>Service:</strong> ${input.serviceName}</p>
|
|
540
|
-
<p><strong>Barber:</strong> ${input.barberName}</p>
|
|
541
|
-
<p><strong>Date:</strong> ${input.scheduledAt}</p>
|
|
542
|
-
`,
|
|
543
|
-
}),
|
|
544
|
-
});
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
The filename becomes the template key: `appointment-confirmation.ts` maps to `email.sendTemplate({ template: "appointmentConfirmation", ... })`.
|
|
548
|
-
|
|
549
|
-
### Sending Emails
|
|
550
|
-
|
|
551
|
-
Use `email.sendTemplate()` from any handler:
|
|
552
|
-
|
|
553
|
-
```ts
|
|
554
|
-
await email.sendTemplate({
|
|
555
|
-
template: "appointmentConfirmation",
|
|
556
|
-
to: customer.email,
|
|
557
|
-
input: {
|
|
558
|
-
customerName: customer.name,
|
|
559
|
-
appointmentId: appointment.id,
|
|
560
|
-
barberName: appointment.barber.name,
|
|
561
|
-
serviceName: appointment.service.name,
|
|
562
|
-
scheduledAt: appointment.scheduledAt.toISOString(),
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Dynamic Email Templates
|
|
568
|
-
|
|
569
|
-
Email handlers receive the full `AppContext` for fetching data:
|
|
570
|
-
|
|
571
|
-
```ts
|
|
572
|
-
// emails/weekly-digest.ts
|
|
573
|
-
import { email } from "questpie/services";
|
|
574
|
-
import { z } from "zod";
|
|
575
|
-
|
|
576
|
-
export default email({
|
|
577
|
-
name: "weekly-digest",
|
|
578
|
-
schema: z.object({ userId: z.string() }),
|
|
579
|
-
handler: async ({ input, collections }) => {
|
|
580
|
-
const user = await collections.users.findOne({
|
|
581
|
-
where: { id: input.userId },
|
|
582
|
-
});
|
|
583
|
-
const recentPosts = await collections.posts.find({
|
|
584
|
-
where: { createdAt: { gte: oneWeekAgo() } },
|
|
585
|
-
limit: 5,
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
subject: `Weekly digest for ${user.name}`,
|
|
590
|
-
html: renderDigestHtml(user, recentPosts.docs),
|
|
591
|
-
text: renderDigestText(user, recentPosts.docs),
|
|
592
|
-
};
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### Email Result
|
|
598
|
-
|
|
599
|
-
| Property | Type | Required | Description |
|
|
600
|
-
| --------- | -------- | -------- | ------------------- |
|
|
601
|
-
| `subject` | `string` | Yes | Email subject line |
|
|
602
|
-
| `html` | `string` | Yes | HTML body |
|
|
603
|
-
| `text` | `string` | No | Plain text fallback |
|
|
604
|
-
|
|
605
|
-
## Common Mistakes
|
|
606
|
-
|
|
607
|
-
1. **HIGH: Putting business logic in route files.**
|
|
608
|
-
Framework route files should only mount HTTP handlers. Business logic belongs in routes, hooks, or services. Raw routes are for HTTP control (webhooks, file downloads, streaming).
|
|
609
|
-
|
|
610
|
-
2. **HIGH: Not using `export default` on route/job/service/email files.**
|
|
611
|
-
Codegen discovery requires `export default`. Named exports are not discovered.
|
|
612
|
-
|
|
613
|
-
3. **MEDIUM: Accessing `app.collections`/`app.globals` without context.**
|
|
614
|
-
For server-side calls to the collection API, you must create a context first: `const ctx = await app.createContext({ accessMode: "system" })`. Then pass it: `app.collections.posts.find({}, ctx)`.
|
|
615
|
-
|
|
616
|
-
4. **MEDIUM: Defining job handlers without queue configuration.**
|
|
617
|
-
Jobs require a queue adapter in production config. Without it, `queue.jobName.publish()` calls will fail at runtime. Configure via `runtimeConfig({ queue: { adapter: pgBossAdapter(...) } })`.
|
|
618
|
-
|
|
619
|
-
5. **MEDIUM: Confusing route keys with filenames.**
|
|
620
|
-
Filenames use kebab-case (`get-active-barbers.ts`) but the key is camelCase (`getActiveBarbers`). The client SDK uses the camelCase key: `client.routes.getActiveBarbers()`.
|