create-qpq-app 0.1.6 → 0.1.7

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.
@@ -23,7 +23,7 @@ deploys as a single docker image.
23
23
  Next steps:
24
24
 
25
25
  cd ${answers.appName}
26
- ${install} npm run go:dev # local dev server on http://localhost:8080
26
+ ${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
27
27
  npm run go # build the docker image (then run the printed docker command)
28
28
 
29
29
  Deploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).
@@ -1 +1 @@
1
- {"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":";;;;;;;;;;;;AAEa,QAAA,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAoB,EAAE,4CAAf,EAAE,OAAO,EAAE;QACrB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC,CAAA;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # local dev server on http://localhost:8080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
1
+ {"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":";;;;;;;;;;;;AAEa,QAAA,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAoB,EAAE,4CAAf,EAAE,OAAO,EAAE;QACrB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC,CAAA;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
@@ -11,7 +11,7 @@ deploys as a single docker image.
11
11
  Next steps:
12
12
 
13
13
  cd ${answers.appName}
14
- ${install} npm run go:dev # local dev server on http://localhost:8080
14
+ ${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
15
15
  npm run go # build the docker image (then run the printed docker command)
16
16
 
17
17
  Deploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).
@@ -1 +1 @@
1
- {"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # local dev server on http://localhost:8080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
1
+ {"version":3,"file":"013_printNextSteps.js","sourceRoot":"","sources":["../../../src/steps/013_printNextSteps.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAqB;IAC9C,IAAI,EAAE,MAAM;IAEZ,GAAG,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;QACzB,MAAM,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kCAAkC,CAAC;QAEtF,OAAO,CAAC,GAAG,CAAC;UACN,OAAO,CAAC,OAAO;;;;;;;OAOlB,OAAO,CAAC,OAAO;EACpB,OAAO;;;8BAGqB,OAAO,CAAC,OAAO,gCAAgC,OAAO,CAAC,MAAM;CAC1F,CAAC,CAAC;IACD,CAAC;CACF,CAAC","sourcesContent":["import { CreateQpqAppStep } from '../types';\n\nexport const printNextSteps: CreateQpqAppStep = {\n name: 'Done',\n\n run: async ({ answers }) => {\n const install = answers.installDependencies ? '' : ' npm install\\n npm run build\\n';\n\n console.log(`\nCreated ${answers.appName}!\n\nYour app has five services — admin, auth, design, shell and todo — and\ndeploys as a single docker image.\n\nNext steps:\n\n cd ${answers.appName}\n${install} npm run go:dev # api on http://localhost:8080, web on http://localhost:3080\n npm run go # build the docker image (then run the printed docker command)\n\nDeploy config lives in apps/${answers.appName}/deploy.config.json (domain: ${answers.domain}).\n`);\n },\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-qpq-app",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Scaffold a new quidproquo app — npx create-qpq-app my-app",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.13.13",
54
- "quidproquo-tsconfig": "0.1.6"
54
+ "quidproquo-tsconfig": "0.1.7"
55
55
  },
56
56
  "bin": {
57
57
  "create-qpq-app": "./lib/commonjs/bin/createQpqApp.js"
@@ -21,8 +21,7 @@ always runs against the in-repo HEAD. Build the framework first
21
21
  nvm use # ALWAYS FIRST — switches to the Node version in .nvmrc
22
22
  npm install # installs deps + symlinks the sibling quidproquo-* packages (file: refs)
23
23
  npm run build # build every workspace (libs -> tsc -b to dist; services -> type-check)
24
- npm run go:dev # backend dev server on :8080 (all services of the selected app)
25
- npm run go:dev:web # views dev servers (host + one Rspack server per remote)
24
+ npm run go:dev # full local dev stack: api dev server (:8080) + all views dev servers
26
25
  ```
27
26
 
28
27
  Smoke test once `go:dev` is up:
@@ -74,7 +73,7 @@ generic CDK app in `quidproquo-deploy-awscdk`.
74
73
  `defineDevServerOptions({ port })` in its `infrastructure.ts`). The
75
74
  only generated artifact is `apps/<app>/tsconfig.federated.json` — regenerate
76
75
  with `npm run prep` after adding or removing a marker.
77
- - **The dev server** (`qpq go:dev`, powered by `quidproquo-dev-server`) loads each service's
76
+ - **The dev server** (`qpq go:dev:api`, powered by `quidproquo-dev-server`) loads each service's
78
77
  `infrastructure.ts` directly — no synth needed, just built libs. Its bundle
79
78
  externalizes `node_modules` (an externals function in its rspack config) so native modules like
80
79
  `sqlite3` load at runtime from their install location.
@@ -90,7 +89,8 @@ generic CDK app in `quidproquo-deploy-awscdk`.
90
89
  npm run build # build only the libs (services/views are bundled, not built); scope with --workspace=@qpqjs/constants
91
90
  npm run validate-ts # TS check: build libs, then tsc-typecheck every service + views
92
91
  npm run prep # regenerate tsconfig.federated.json from // federated.export markers
93
- npm run go:dev # backend dev server on :8080, every service at /api/<svc>
92
+ npm run go:dev # api + web dev servers in one process (one ctrl+c stops the lot)
93
+ npm run go:dev:api # backend dev server only, on :8080, every service at /api/<svc>
94
94
  npm run go:dev:web # boot ALL views Rspack dev servers (host shell :3080 + remotes on their
95
95
  # ports); remotes resolve dynamically at runtime via mf-manifest.json.
96
96
  # Subset: npm run go:dev:web -- --only host,shell,design
@@ -99,7 +99,7 @@ npm run go:dev:web # boot ALL views Rspack dev servers (host shell :3080 + rem
99
99
  ### App selection (multi-app)
100
100
 
101
101
  Every app-facing script (`prep`, `synth`, `go`, `go:docker`, `go:dev`,
102
- `go:dev:web`) resolves its target app the same way:
102
+ `go:dev:api`, `go:dev:web`) resolves its target app the same way:
103
103
 
104
104
  1. `--app <name>` — **must come after `--`**: `npm run go:dev -- --app qpqjs`
105
105
  (this npm silently swallows flags before `--`)
@@ -18,8 +18,9 @@ export function Footer() {
18
18
  <span className="hero__title-glow">your first story?</span>
19
19
  </h2>
20
20
  <p className="footer__cta-sub">
21
- Install the core, yield an action, and let the runtime handle the
22
- rest.
21
+ One command scaffolds a full app five services, a local dev
22
+ server, a one-image docker deploy. Yield an action; the runtime
23
+ handles the rest.
23
24
  </p>
24
25
  <div className="footer__cta-actions">
25
26
  <InstallChip />
@@ -1,6 +1,6 @@
1
1
  import { useState } from 'react';
2
2
 
3
- const INSTALL_COMMAND = 'npm install quidproquo-core';
3
+ const INSTALL_COMMAND = 'npx create-qpq-app myapp';
4
4
 
5
5
  export function InstallChip() {
6
6
  const [copied, setCopied] = useState(false);
@@ -42,7 +42,7 @@ The parameters are identical to [askAiPrompt](./ask-ai-prompt.md) — see there
42
42
  | --- | --- | --- |
43
43
  | `model` | `AiModel` | Which model to prompt. |
44
44
  | `prompt` | `string` | The user prompt. Ignored when `options.messages` is set. |
45
- | `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, reasoning? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
45
+ | `options` | `AskAiPromptStreamOptions` | `{ system?, aiName?, messages?, reasoning?, caching? }` — same shape and meaning as [`AskAiPromptOptions`](./ask-ai-prompt.md#askaipromptoptions). |
46
46
 
47
47
  ## Returns
48
48
 
@@ -49,6 +49,7 @@ function* askAiPrompt(
49
49
  | `aiName` | `string` | – | Name of a [defineAi](../../../config/core/ai.md) config to bind. This is what wires up tool definitions (and their executors) for the model to call. Omit for a plain, tool-less prompt. |
50
50
  | `messages` | [`AiMessage[]`](#aimessage) | – | A full conversation history. When present, this is sent instead of `prompt`, letting you carry a multi-turn dialogue (including prior assistant turns and tool results). |
51
51
  | `reasoning` | [`AiReasoningConfig`](#aireasoningconfig) | – | Enables extended thinking. Its presence turns reasoning on; `budgetTokens` caps how many tokens the model may spend thinking before it answers (defaults to `4096` on AWS). |
52
+ | `caching` | `boolean` | – | Marks the system prompt and the last message (or the last `messages` entry) with a Bedrock cache point, so a following call in the same conversation can read everything up to there from cache instead of reprocessing it. |
52
53
 
53
54
  ### `AiModel`
54
55
 
@@ -4,803 +4,142 @@ sidebar_position: 1
4
4
 
5
5
  # Getting Started
6
6
 
7
- Get up and running with Quidproquo in minutes. This guide will walk you through installation, creating your first story, building an API, and deploying your application.
8
-
9
- ## Prerequisites
10
-
11
- Before you begin, ensure you have:
12
- - Node.js 18+ installed
13
- - npm or yarn package manager
14
- - Basic knowledge of JavaScript/TypeScript
15
- - Familiarity with generator functions (we'll explain the basics)
16
-
17
- ## Installation
18
-
19
- ### Create a New Project
7
+ The fastest way to a running quidproquo app is one command:
20
8
 
21
9
  ```bash
22
- # Create a new directory
23
- mkdir my-qpq-app
24
- cd my-qpq-app
25
-
26
- # Initialize package.json
27
- npm init -y
28
-
29
- # Install TypeScript (recommended)
30
- npm install --save-dev typescript @types/node
10
+ npx create-qpq-app myapp
31
11
  ```
32
12
 
33
- ### Install Quidproquo Packages
13
+ That scaffolds a complete, working application — five services, a local dev
14
+ server, and a one-image docker deploy — then installs its dependencies,
15
+ builds it, and makes the first git commit for you.
34
16
 
35
17
  ```bash
36
- # Core package (required)
37
- npm install quidproquo-core
38
-
39
- # Web server package (for APIs)
40
- npm install quidproquo-webserver
41
-
42
- # Development server (for local development)
43
- npm install --save-dev quidproquo-dev-server
44
-
45
- # Platform-specific processors (choose based on deployment target)
46
- npm install quidproquo-actionprocessor-node # For Node.js
47
- # OR
48
- npm install quidproquo-actionprocessor-awslambda # For AWS
49
- ```
50
-
51
- ### TypeScript Configuration
52
-
53
- Create a `tsconfig.json`:
54
-
55
- ```json
56
- {
57
- "compilerOptions": {
58
- "target": "ES2020",
59
- "module": "commonjs",
60
- "lib": ["ES2020"],
61
- "outDir": "./dist",
62
- "rootDir": "./src",
63
- "strict": true,
64
- "esModuleInterop": true,
65
- "skipLibCheck": true,
66
- "forceConsistentCasingInFileNames": true,
67
- "resolveJsonModule": true,
68
- "declaration": true,
69
- "declarationMap": true
70
- },
71
- "include": ["src/**/*"],
72
- "exclude": ["node_modules", "dist"]
73
- }
74
- ```
75
-
76
- ## Your First Story
77
-
78
- Let's create a simple "Hello World" story to understand the basics.
79
-
80
- ### Understanding Generator Functions
81
-
82
- Generator functions (marked with `*`) can pause and resume execution:
83
-
84
- ```typescript
85
- // src/stories/hello.ts
86
- import { askDateNow, askLogCreate } from 'quidproquo-core';
87
-
88
- export function* helloWorldStory(name: string) {
89
- // Log the start
90
- yield* askLogCreate('INFO', `Starting hello for ${name}`);
91
-
92
- // Get current time
93
- const currentTime = yield* askDateNow();
94
-
95
- // Return greeting
96
- return `Hello ${name}! The time is ${currentTime}`;
97
- }
98
- ```
99
-
100
- **Key concepts:**
101
- - `function*` declares a generator function
102
- - `yield*` pauses execution and delegates to another generator
103
- - Each `ask*` function returns a generator that yields an action
104
- - The runtime processes each action and returns the result
105
-
106
- ### Creating Actions
107
-
108
- Actions describe what you want to do:
109
-
110
- ```typescript
111
- // src/stories/user.ts
112
- import {
113
- askKeyValueStoreGet,
114
- askKeyValueStoreUpsert,
115
- askGuidNew,
116
- askDateNow,
117
- askThrowError
118
- } from 'quidproquo-core';
119
-
120
- export function* createUserStory(email: string, name: string) {
121
- // Check if user exists
122
- const existingUser = yield* askKeyValueStoreGet('users', email);
123
-
124
- if (existingUser) {
125
- // Throw an error if user exists
126
- yield* askThrowError('USER_EXISTS', `User with email ${email} already exists`);
127
- }
128
-
129
- // Generate a new ID
130
- const userId = yield* askGuidNew();
131
-
132
- // Get current timestamp
133
- const createdAt = yield* askDateNow();
134
-
135
- // Create user object
136
- const user = {
137
- id: userId,
138
- email,
139
- name,
140
- createdAt
141
- };
142
-
143
- // Store the user
144
- yield* askKeyValueStoreUpsert('users', user);
145
-
146
- return user;
147
- }
18
+ cd myapp
19
+ npm run go:dev # api on http://localhost:8080, web on http://localhost:3080
148
20
  ```
149
21
 
150
- ## Building Your First API
151
-
152
- Now let's create a REST API using Quidproquo.
153
-
154
- ### Define Routes
22
+ One command, one ctrl+c: `go:dev` runs the backend dev server and every views
23
+ dev server in a single process. Want just one side? `npm run go:dev:api` and
24
+ `npm run go:dev:web` run them separately.
155
25
 
156
- ```typescript
157
- // src/api/index.ts
158
- import { HTTPMethod, HTTPResponse } from 'quidproquo-webserver';
159
- import { createUserStory } from '../stories/user';
160
- import { askCatch } from 'quidproquo-core';
26
+ Check it's alive:
161
27
 
162
- export function* handleCreateUser(request: any): Generator<any, HTTPResponse, any> {
163
- const { email, name } = JSON.parse(request.body);
164
-
165
- // Validate input
166
- if (!email || !name) {
167
- return {
168
- statusCode: 400,
169
- headers: { 'Content-Type': 'application/json' },
170
- body: JSON.stringify({ error: 'Email and name are required' })
171
- };
172
- }
173
-
174
- // Try to create user
175
- const result = yield* askCatch(createUserStory(email, name));
176
-
177
- if (result.success) {
178
- return {
179
- statusCode: 201,
180
- headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify(result.result)
182
- };
183
- } else {
184
- return {
185
- statusCode: 400,
186
- headers: { 'Content-Type': 'application/json' },
187
- body: JSON.stringify({ error: result.error.errorText })
188
- };
189
- }
190
- }
191
- ```
192
-
193
- ### Configure the API
194
-
195
- ```typescript
196
- // src/config.ts
197
- import { defineRoute, defineApi, defineService } from 'quidproquo-webserver';
198
- import { defineKeyValueStore } from 'quidproquo-core';
199
- import { handleCreateUser } from './api';
200
-
201
- export default [
202
- // Define a key-value store for users
203
- defineKeyValueStore('users', {
204
- partitionKey: 'email',
205
- indexes: []
206
- }),
207
-
208
- // Define API routes
209
- defineApi('main', {
210
- routes: [
211
- defineRoute(HTTPMethod.POST, '/users', handleCreateUser),
212
- defineRoute(HTTPMethod.GET, '/health', function* () {
213
- return {
214
- statusCode: 200,
215
- body: JSON.stringify({ status: 'healthy' })
216
- };
217
- })
218
- ]
219
- }),
220
-
221
- // Define the service
222
- defineService('my-app', {
223
- apis: ['main']
224
- })
225
- ];
28
+ ```bash
29
+ curl http://localhost:8080/api/shell/v1/health
30
+ # {"status":"healthy","service":"shell","checkedAt":"..."}
226
31
  ```
227
32
 
228
- ## Running Locally
229
-
230
- The development server simulates cloud services locally.
33
+ ## Prerequisites
231
34
 
232
- ### Create Dev Server Entry
35
+ - **Node.js 24 or newer** — the scaffolder checks and refuses politely on
36
+ older versions
37
+ - **Docker** — only needed when you deploy (`npm run go`); local dev doesn't
38
+ use it
233
39
 
234
- ```typescript
235
- // src/dev.ts
236
- import { createDevServer } from 'quidproquo-dev-server';
237
- import config from './config';
40
+ ## What you get
238
41
 
239
- const server = createDevServer({
240
- config,
241
- port: 3000,
242
- watch: true
243
- });
42
+ `create-qpq-app` generates an npm-workspaces monorepo:
244
43
 
245
- server.start().then(() => {
246
- console.log('Dev server running on http://localhost:3000');
247
- });
248
44
  ```
249
-
250
- ### Add Scripts to package.json
251
-
252
- ```json
253
- {
254
- "scripts": {
255
- "dev": "ts-node src/dev.ts",
256
- "build": "tsc",
257
- "start": "node dist/dev.js"
258
- }
259
- }
45
+ myapp/
46
+ ├── package.json # workspace root — run all commands from here
47
+ └── apps/myapp/
48
+ ├── deploy.config.json # environments: development + production (docker)
49
+ ├── packages/
50
+ │ ├── constants/ # domain, service enum, user directory name
51
+ │ ├── config/
52
+ │ └── service-utils/ # the shared config every service starts from
53
+ └── services/
54
+ ├── shell/ # module-federation host — the root website
55
+ ├── design/ # shared UI — federated views other services import
56
+ ├── auth/ # authentication service
57
+ ├── admin/ # the qpq admin console
58
+ └── todo/ # the example feature: an api service + TodoList view
260
59
  ```
261
60
 
262
- ### Start the Dev Server
61
+ Every service follows the same layout: a `service/` package holding its
62
+ infrastructure config and controllers, an optional `views/` microfrontend,
63
+ and small lib packages (`models`, `shared-logic`, `service-utils`, …) for
64
+ code you'll share between them.
263
65
 
264
- ```bash
265
- npm run dev
266
- ```
267
-
268
- Your API is now running! Test it:
66
+ ## Your first story
269
67
 
270
- ```bash
271
- # Create a user
272
- curl -X POST http://localhost:3000/users \
273
- -H "Content-Type: application/json" \
274
- -d '{"email": "alice@example.com", "name": "Alice"}'
68
+ Business logic in quidproquo is a **story** — a generator function that
69
+ yields typed actions. The scaffolded shell service ships one: the health
70
+ route you just curled.
275
71
 
276
- # Check health
277
- curl http://localhost:3000/health
278
- ```
279
-
280
- ## Complete Example: Todo API
281
-
282
- Let's build a more complete example - a Todo API with CRUD operations.
283
-
284
- ### Define Todo Stories
285
-
286
- ```typescript
287
- // src/stories/todo.ts
72
+ ```typescript title="apps/myapp/services/shell/service/src/entry/controller/health/health.ts"
288
73
  import {
289
- askKeyValueStoreGet,
290
- askKeyValueStoreUpsert,
291
- askKeyValueStoreDelete,
292
- askKeyValueStoreQuery,
293
- askGuidNew,
294
74
  askDateNow,
295
- askThrowError
296
- } from 'quidproquo-core';
297
-
298
- interface Todo {
299
- id: string;
300
- userId: string;
301
- title: string;
302
- completed: boolean;
303
- createdAt: string;
304
- updatedAt: string;
305
- }
306
-
307
- export function* createTodoStory(userId: string, title: string) {
308
- const todoId = yield* askGuidNew();
309
- const now = yield* askDateNow();
310
-
311
- const todo: Todo = {
312
- id: todoId,
313
- userId,
314
- title,
315
- completed: false,
316
- createdAt: now,
317
- updatedAt: now
318
- };
319
-
320
- yield* askKeyValueStoreUpsert('todos', todo);
321
- return todo;
322
- }
323
-
324
- export function* getTodoStory(todoId: string, userId: string) {
325
- const todo = yield* askKeyValueStoreGet('todos', todoId);
326
-
327
- if (!todo) {
328
- yield* askThrowError('NOT_FOUND', 'Todo not found');
329
- }
330
-
331
- if (todo.userId !== userId) {
332
- yield* askThrowError('FORBIDDEN', 'Access denied');
333
- }
334
-
335
- return todo;
336
- }
337
-
338
- export function* updateTodoStory(
339
- todoId: string,
340
- userId: string,
341
- updates: Partial<Pick<Todo, 'title' | 'completed'>>
342
- ) {
343
- // Get existing todo
344
- const todo = yield* getTodoStory(todoId, userId);
345
-
346
- // Update fields
347
- const updatedTodo = {
348
- ...todo,
349
- ...updates,
350
- updatedAt: yield* askDateNow()
351
- };
352
-
353
- yield* askKeyValueStoreUpsert('todos', updatedTodo);
354
- return updatedTodo;
355
- }
356
-
357
- export function* deleteTodoStory(todoId: string, userId: string) {
358
- // Verify ownership
359
- yield* getTodoStory(todoId, userId);
360
-
361
- // Delete
362
- yield* askKeyValueStoreDelete('todos', todoId);
363
- return { success: true };
364
- }
365
-
366
- export function* listTodosStory(userId: string) {
367
- const todos = yield* askKeyValueStoreQuery('todos', {
368
- index: 'byUser',
369
- keyCondition: {
370
- userId: { '=': userId }
371
- }
372
- });
373
-
374
- return todos;
375
- }
376
- ```
377
-
378
- ### Create API Handlers
379
-
380
- ```typescript
381
- // src/api/todos.ts
382
- import { HTTPRequest, HTTPResponse } from 'quidproquo-webserver';
383
- import { askCatch } from 'quidproquo-core';
384
- import * as todoStories from '../stories/todo';
385
-
386
- export function* createTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
387
- const { title } = JSON.parse(request.body);
388
- const userId = request.headers['x-user-id']; // Simplified auth
389
-
390
- if (!title) {
391
- return { statusCode: 400, body: JSON.stringify({ error: 'Title required' }) };
392
- }
393
-
394
- const todo = yield* todoStories.createTodoStory(userId, title);
395
- return { statusCode: 201, body: JSON.stringify(todo) };
396
- }
397
-
398
- export function* getTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
399
- const todoId = request.pathParameters.id;
400
- const userId = request.headers['x-user-id'];
401
-
402
- const result = yield* askCatch(todoStories.getTodoStory(todoId, userId));
403
-
404
- if (!result.success) {
405
- const statusCode = result.error.errorType === 'NOT_FOUND' ? 404 : 403;
406
- return { statusCode, body: JSON.stringify({ error: result.error.errorText }) };
407
- }
408
-
409
- return { statusCode: 200, body: JSON.stringify(result.result) };
410
- }
411
-
412
- export function* updateTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
413
- const todoId = request.pathParameters.id;
414
- const userId = request.headers['x-user-id'];
415
- const updates = JSON.parse(request.body);
416
-
417
- const result = yield* askCatch(
418
- todoStories.updateTodoStory(todoId, userId, updates)
419
- );
420
-
421
- if (!result.success) {
422
- return { statusCode: 400, body: JSON.stringify({ error: result.error.errorText }) };
423
- }
424
-
425
- return { statusCode: 200, body: JSON.stringify(result.result) };
426
- }
427
-
428
- export function* deleteTodo(request: HTTPRequest): Generator<any, HTTPResponse, any> {
429
- const todoId = request.pathParameters.id;
430
- const userId = request.headers['x-user-id'];
431
-
432
- const result = yield* askCatch(todoStories.deleteTodoStory(todoId, userId));
433
-
434
- if (!result.success) {
435
- return { statusCode: 400, body: JSON.stringify({ error: result.error.errorText }) };
75
+ AskResponse,
76
+ HTTPEvent,
77
+ HTTPEventResponse,
78
+ qpqWebServerUtils,
79
+ } from 'quidproquo';
80
+ import { dynamicRoute } from 'quidproquo-features';
81
+
82
+ export const health = dynamicRoute(
83
+ ['GET', '/health'],
84
+ function* healthCheck(event: HTTPEvent): AskResponse<HTTPEventResponse> {
85
+ const checkedAt = yield* askDateNow();
86
+
87
+ return qpqWebServerUtils.toJsonEventResponse({
88
+ status: 'healthy',
89
+ service: 'shell',
90
+ checkedAt,
91
+ });
436
92
  }
437
-
438
- return { statusCode: 204, body: '' };
439
- }
440
-
441
- export function* listTodos(request: HTTPRequest): Generator<any, HTTPResponse, any> {
442
- const userId = request.headers['x-user-id'];
443
- const todos = yield* todoStories.listTodosStory(userId);
444
- return { statusCode: 200, body: JSON.stringify(todos) };
445
- }
93
+ );
446
94
  ```
447
95
 
448
- ### Configure Complete API
449
-
450
- ```typescript
451
- // src/config/todos.ts
452
- import { defineRoute, defineApi, HTTPMethod } from 'quidproquo-webserver';
453
- import { defineKeyValueStore } from 'quidproquo-core';
454
- import * as todoHandlers from '../api/todos';
455
-
456
- export default [
457
- // Define storage
458
- defineKeyValueStore('todos', {
459
- partitionKey: 'id',
460
- indexes: [
461
- {
462
- name: 'byUser',
463
- partitionKey: 'userId',
464
- sortKey: 'createdAt'
465
- }
466
- ]
467
- }),
468
-
469
- // Define API
470
- defineApi('todos', {
471
- routes: [
472
- defineRoute(HTTPMethod.POST, '/todos', todoHandlers.createTodo),
473
- defineRoute(HTTPMethod.GET, '/todos', todoHandlers.listTodos),
474
- defineRoute(HTTPMethod.GET, '/todos/:id', todoHandlers.getTodo),
475
- defineRoute(HTTPMethod.PUT, '/todos/:id', todoHandlers.updateTodo),
476
- defineRoute(HTTPMethod.DELETE, '/todos/:id', todoHandlers.deleteTodo),
477
- ]
478
- })
479
- ];
480
- ```
96
+ The story never reads the clock itself — it *asks* for the time by yielding
97
+ an action, and the runtime answers. On your machine that's the dev server;
98
+ in production it's whatever platform the app is deployed to. Same story,
99
+ zero changes.
481
100
 
482
- ## Deployment Options
101
+ Add your own route by dropping a file next to `health.ts`, exporting it from
102
+ the controller `index.ts`, and saving — the dev server rebuilds and restarts
103
+ itself.
483
104
 
484
- Quidproquo supports multiple deployment targets. Here's how to deploy to common platforms.
105
+ ## Deploy it
485
106
 
486
- ### Deploy to AWS Lambda
107
+ Both scaffolded environments deploy as a single docker image — the whole
108
+ app, one container:
487
109
 
488
110
  ```bash
489
- # Install AWS CDK and QPQ deploy package
490
- npm install --save-dev aws-cdk quidproquo-deploy-awscdk
491
- npm install quidproquo-actionprocessor-awslambda
492
-
493
- # Create CDK app
494
- mkdir cdk
495
- cd cdk
496
- cdk init app --language typescript
497
- ```
498
-
499
- Create CDK stack:
500
-
501
- ```typescript
502
- // cdk/lib/my-app-stack.ts
503
- import * as cdk from 'aws-cdk-lib';
504
- import { QPQApp } from 'quidproquo-deploy-awscdk';
505
- import config from '../../src/config';
506
-
507
- export class MyAppStack extends cdk.Stack {
508
- constructor(scope: Construct, id: string, props?: cdk.StackProps) {
509
- super(scope, id, props);
510
-
511
- new QPQApp(this, 'MyQPQApp', {
512
- qpqConfig: config,
513
- environment: 'production'
514
- });
515
- }
516
- }
111
+ npm run go
517
112
  ```
518
113
 
519
- Deploy:
114
+ Pick an environment when prompted (or pass `--env development`), and the
115
+ build prints the `docker run` command when the image is ready:
520
116
 
521
117
  ```bash
522
- cdk deploy
118
+ docker run --rm -p 80:8080 -p 8080:8080 -p 8888:8888 -p 3001:3001 \
119
+ -v qpq-myapp-data:/app/.qpq-runtime \
120
+ qpq-myapp:development
523
121
  ```
524
122
 
525
- ### Deploy to Node.js Server
526
-
527
- ```typescript
528
- // src/server.ts
529
- import express from 'express';
530
- import { createNodeRuntime } from 'quidproquo-actionprocessor-node';
531
- import config from './config';
532
-
533
- const app = express();
534
- const runtime = createNodeRuntime(config);
535
-
536
- app.use(express.json());
537
- app.all('*', async (req, res) => {
538
- const result = await runtime.handleHTTPRequest({
539
- method: req.method,
540
- path: req.path,
541
- headers: req.headers,
542
- body: JSON.stringify(req.body),
543
- query: req.query
544
- });
545
-
546
- res.status(result.statusCode).json(JSON.parse(result.body));
547
- });
548
-
549
- app.listen(3000, () => {
550
- console.log('Server running on port 3000');
551
- });
552
- ```
553
-
554
- ### Deploy to Docker
555
-
556
- Create a Dockerfile:
557
-
558
- ```dockerfile
559
- FROM node:18-alpine
560
-
561
- WORKDIR /app
562
-
563
- COPY package*.json ./
564
- RUN npm ci --production
565
-
566
- COPY dist ./dist
567
- COPY config ./config
123
+ Then open [http://localhost](http://localhost). Environments live in
124
+ `apps/myapp/deploy.config.json` — an app can move platforms (docker → AWS)
125
+ with a config change, not a rewrite.
568
126
 
569
- CMD ["node", "dist/server.js"]
570
- ```
571
-
572
- Build and run:
127
+ ## Options
573
128
 
574
129
  ```bash
575
- npm run build
576
- docker build -t my-qpq-app .
577
- docker run -p 3000:3000 my-qpq-app
578
- ```
579
-
580
- ## Testing Your Application
581
-
582
- Quidproquo's architecture makes testing straightforward.
583
-
584
- ### Unit Testing Stories
585
-
586
- ```typescript
587
- // src/stories/todo.test.ts
588
- import { createTodoStory } from './todo';
589
-
590
- test('creates todo with correct fields', () => {
591
- const story = createTodoStory('user-123', 'Buy milk');
592
-
593
- // First yield: Generate ID
594
- const { value: guidAction } = story.next();
595
- expect(guidAction.type).toBe('Guid::New');
596
-
597
- // Provide mock ID
598
- const { value: dateAction } = story.next('todo-456');
599
- expect(dateAction.type).toBe('Date::Now');
600
-
601
- // Provide mock date
602
- const { value: storeAction } = story.next('2024-01-01T00:00:00Z');
603
- expect(storeAction.type).toBe('KeyValueStore::Upsert');
604
- expect(storeAction.payload.item).toEqual({
605
- id: 'todo-456',
606
- userId: 'user-123',
607
- title: 'Buy milk',
608
- completed: false,
609
- createdAt: '2024-01-01T00:00:00Z',
610
- updatedAt: '2024-01-01T00:00:00Z'
611
- });
612
-
613
- // Complete story
614
- const { value: result } = story.next();
615
- expect(result.id).toBe('todo-456');
616
- });
617
- ```
618
-
619
- ### Integration Testing
620
-
621
- ```typescript
622
- // src/api/todos.test.ts
623
- import { createTestRuntime } from 'quidproquo-testing';
624
- import { createTodo } from './todos';
625
-
626
- test('creates todo via API', async () => {
627
- const runtime = createTestRuntime({
628
- stores: {
629
- todos: new Map()
630
- }
631
- });
632
-
633
- const request = {
634
- body: JSON.stringify({ title: 'Test todo' }),
635
- headers: { 'x-user-id': 'user-123' }
636
- };
637
-
638
- const response = await runtime.execute(createTodo, request);
639
-
640
- expect(response.statusCode).toBe(201);
641
- const todo = JSON.parse(response.body);
642
- expect(todo.title).toBe('Test todo');
643
- expect(todo.userId).toBe('user-123');
644
- });
645
- ```
130
+ npx create-qpq-app <app-name> [options]
646
131
 
647
- ## Debugging
648
-
649
- Quidproquo provides excellent debugging capabilities.
650
-
651
- ### Enable Debug Logging
652
-
653
- ```typescript
654
- // Set environment variable
655
- process.env.QPQ_LOG_LEVEL = 'DEBUG';
656
-
657
- // Or in config
658
- import { defineLogging } from 'quidproquo-core';
659
-
660
- export default [
661
- defineLogging({
662
- level: 'DEBUG',
663
- pretty: true
664
- }),
665
- // ... other config
666
- ];
132
+ --language <typescript|javascript> skip the language prompt
133
+ --domain <domain> app domain (default: <app-name>.example.com)
134
+ --no-git skip git init
135
+ --no-install skip npm install (and the initial build)
667
136
  ```
668
137
 
669
- ### View Execution Logs
670
-
671
- ```typescript
672
- // src/debug.ts
673
- import { createDevServer } from 'quidproquo-dev-server';
674
-
675
- const server = createDevServer({
676
- config,
677
- port: 3000,
678
- debug: {
679
- logActions: true, // Log all actions
680
- logResults: true, // Log action results
681
- logDuration: true, // Log execution time
682
- slowThreshold: 100, // Warn if action takes > 100ms
683
- }
684
- });
685
- ```
686
-
687
- ### Replay Executions
688
-
689
- ```typescript
690
- // Capture execution
691
- const execution = await runtime.execute(myStory, args, {
692
- capture: true
693
- });
694
-
695
- // Save for debugging
696
- fs.writeFileSync('execution.json', JSON.stringify(execution));
697
-
698
- // Replay later
699
- const executionLog = JSON.parse(fs.readFileSync('execution.json'));
700
- const result = await qpqExecuteLog(executionLog, myStory);
701
- ```
702
-
703
- ## Best Practices
704
-
705
- ### 1. Keep Stories Pure
706
-
707
- ```typescript
708
- // Good: Pure story with no side effects
709
- function* calculatePrice(items: Item[]) {
710
- const subtotal = items.reduce((sum, item) => sum + item.price, 0);
711
- const tax = subtotal * 0.1;
712
- return subtotal + tax;
713
- }
714
-
715
- // Bad: Side effects in story
716
- function* calculatePrice(items: Item[]) {
717
- console.log('Calculating...'); // Side effect!
718
- const subtotal = items.reduce((sum, item) => sum + item.price, 0);
719
- fs.writeFileSync('log.txt', `Subtotal: ${subtotal}`); // Side effect!
720
- return subtotal * 1.1;
721
- }
722
- ```
723
-
724
- ### 2. Use Composition
725
-
726
- ```typescript
727
- // Compose smaller stories
728
- function* validateEmail(email: string) {
729
- if (!email.includes('@')) {
730
- yield* askThrowError('INVALID_EMAIL', 'Invalid email format');
731
- }
732
- }
733
-
734
- function* createUser(email: string, name: string) {
735
- yield* validateEmail(email); // Reuse validation
736
- // ... rest of creation logic
737
- }
738
- ```
739
-
740
- ### 3. Handle Errors Gracefully
741
-
742
- ```typescript
743
- function* safeOperation() {
744
- const result = yield* askCatch(riskyOperation());
745
-
746
- if (!result.success) {
747
- // Log error
748
- yield* askLogCreate('ERROR', result.error.errorText);
749
- // Return default
750
- return defaultValue;
751
- }
752
-
753
- return result.result;
754
- }
755
- ```
756
-
757
- ### 4. Use TypeScript
758
-
759
- ```typescript
760
- // Define types for your domain
761
- interface User {
762
- id: string;
763
- email: string;
764
- name: string;
765
- }
766
-
767
- // Type your stories
768
- function* getUser(userId: string): Generator<any, User, any> {
769
- const user = yield* askKeyValueStoreGet<User>('users', userId);
770
- if (!user) {
771
- yield* askThrowError('NOT_FOUND', 'User not found');
772
- }
773
- return user;
774
- }
775
- ```
776
-
777
- ## Next Steps
778
-
779
- Now that you have the basics, explore:
780
-
781
- - [Core Concepts](./core-concepts.md) - Deep dive into the architecture
782
- - [API Reference](./api/index.md) - Complete action reference
783
- - [Use Cases](./use-cases.md) - Real-world examples
784
- - [Architecture Overview](./architecture-overview.md) - Understanding the internals
785
-
786
- ## Getting Help
787
-
788
- - **Documentation**: This site contains comprehensive documentation
789
- - **GitHub Issues**: Report bugs or request features
790
- - **Examples**: Check the examples directory in the repository
791
- - **Community**: Join our Discord server (coming soon)
792
-
793
- ## Summary
794
-
795
- You've learned how to:
796
- - ✅ Install Quidproquo
797
- - ✅ Create your first story
798
- - ✅ Build a REST API
799
- - ✅ Run locally with the dev server
800
- - ✅ Deploy to various platforms
801
- - ✅ Test your application
802
- - ✅ Debug issues
138
+ Prefer plain JavaScript? `--language javascript` generates the same app with
139
+ type annotations stripped and JSX preserved — it runs on the same toolchain.
803
140
 
804
- Quidproquo provides a powerful, flexible foundation for building modern web applications. Its unique architecture enables you to write business logic once and deploy anywhere, with complete observability and testing capabilities built in.
141
+ ## Next steps
805
142
 
806
- Start building with Quidproquo today and experience the benefits of true platform independence!
143
+ - [Core Concepts](./core-concepts.md) stories, actions and processors in depth
144
+ - [Architecture Overview](./architecture-overview.md) — how the runtime executes a story
145
+ - [API Reference](./api/index.md) — the full action catalogue
@@ -179,7 +179,12 @@ Dive deep into the architecture that powers Quidproquo's platform independence a
179
179
 
180
180
  ## Next Steps
181
181
 
182
- Ready to get started with Quidproquo?
182
+ Ready to get started with Quidproquo? One command scaffolds a complete,
183
+ running app:
184
+
185
+ ```bash
186
+ npx create-qpq-app myapp
187
+ ```
183
188
 
184
189
  <div className="row">
185
190
  <div className="col col--6">
@@ -188,7 +193,7 @@ Ready to get started with Quidproquo?
188
193
  <h3>🚀 Quick Start</h3>
189
194
  </div>
190
195
  <div className="card__body">
191
- <p>Get up and running in 5 minutes with our quick start guide.</p>
196
+ <p>Scaffold a five-service app that runs locally and deploys as a single docker image.</p>
192
197
  </div>
193
198
  <div className="card__footer">
194
199
  <a href="/getting-started" className="button button--primary button--block">Get Started</a>
@@ -25,8 +25,9 @@
25
25
  "lint:fix": "eslint . --fix",
26
26
  "synth": "qpq synth",
27
27
  "go:dev": "qpq go:dev",
28
- "prep": "qpq prep",
28
+ "go:dev:api": "qpq go:dev:api",
29
29
  "go:dev:web": "qpq go:dev:web",
30
+ "prep": "qpq prep",
30
31
  "postinstall": "qpq hooks postinstall || true",
31
32
  "go": "qpq go",
32
33
  "publish": "qpq publish",