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.
Files changed (152) hide show
  1. package/dist/index.mjs +362 -119
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. 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()`.