nibula 1.0.1 → 1.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/README.md CHANGED
@@ -16,11 +16,12 @@ Building a website from scratch involves a lot of moving parts: templating, buil
16
16
  - ðŸ–ąïļ **A helpful CLI** — create a page with one command instead of hand-writing ten separate files
17
17
  - ⚙ïļ **Server configs handled for you** — `.htaccess` and `web.config` are generated automatically, and an `nginx.conf` is provided so that anyone comfortable with nginx already has what they need to run the site outside of shared hosting
18
18
  - ðŸŽĻ **Pick your CSS framework** — choose from 4 pre-installed options (or none), and switch later in a few guided steps
19
+ - 🔌 **Pick your backend** — Node.js or PHP, chosen at creation; **Composer is only needed for PHP**
19
20
  - ðŸ§Đ **Your own modules** — add your own CSS and JS/TS modules freely and easily
20
21
  - ðŸŠķ **Lightweight by default** — SCSS frameworks can be filtered so you ship only what you actually use
21
22
  - 🌍 **Open source** — free to use, free to modify, free to share
22
23
 
23
- ![Version](https://img.shields.io/badge/version-2.9.0-blue)
24
+ ![Version](https://img.shields.io/badge/version-1.1.0-blue)
24
25
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
25
26
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
26
27
 
@@ -35,7 +36,7 @@ Either way, the project structure stays the same, so you can start easy and leve
35
36
 
36
37
  ## Backend included
37
38
 
38
- Essential server-side functionality comes built in — no extra setup required. Backend support will soon let you **choose between PHP, Node, and Python**, so the project can grow with you.
39
+ Essential server-side functionality comes built in — no extra setup required. At project creation you **choose your backend: Node.js or PHP** (Python is planned). Both expose the **same REST API** — same routing, `X-Api-Key` auth, CORS and rate limiting — so you can even switch later without rewriting your endpoints. See [docs/Backend.md](docs/Backend.md) for details.
39
40
 
40
41
  ## Customizable, but with sensible defaults
41
42
 
@@ -43,8 +44,8 @@ Nibula ships with a clean, opinionated layout so beginners are never lost. But i
43
44
 
44
45
  ## Prerequisites
45
46
 
46
- * **Node.js**: v18.0.0 or higher
47
- * **Composer**: latest stable version
47
+ * **Node.js**: v18.0.0 or higher — **always required** (the Nibula CLI, the build, and the optional Node backend all run on Node)
48
+ * **Composer**: **only required if you choose the PHP backend**, to install its PHP dependencies. If you pick the **Node** backend, Composer is never used — you can skip installing it entirely.
48
49
  * *Optional:* the **Better Nunjucks** VS Code extension by Ed Heltzel
49
50
 
50
51
  ## Installation
@@ -65,7 +66,7 @@ From the folder where you keep your websites, run:
65
66
  nib new your-project
66
67
  ```
67
68
 
68
- The scaffolder is interactive: you choose the language (JavaScript/TypeScript) and the CSS framework, and all dependencies are installed automatically.
69
+ The scaffolder is interactive: you choose the **language** (JavaScript/TypeScript), the **CSS framework**, and the **backend** (Node.js or PHP). All dependencies are installed automatically — and if you pick **Node**, the PHP/Composer step is skipped, so you don't need Composer at all.
69
70
 
70
71
  Then start the dev server and visit `localhost:8080`:
71
72
 
@@ -99,7 +100,20 @@ nib cli
99
100
 
100
101
  See [docs/Assistant CLI.md](docs/Assistant%20CLI.md) for details.
101
102
 
103
+ ## Deploying
104
+
105
+ Nibula builds a static site into your `out` folder, which you upload to a web
106
+ server. Which backend you chose affects deployment:
107
+
108
+ - **PHP** runs on ordinary shared hosting (Apache/IIS) or a VPS with PHP-FPM — no process to keep alive.
109
+ - **Node** runs as a long-running service on a VPS; the web server reverse-proxies `/api` to it.
110
+
111
+ The shipped `.htaccess`, `web.config` and `nginx.conf` cover **both** backends.
112
+ See [docs/Deploy.md](docs/Deploy.md) for the full guide, including how to start
113
+ the Node service on your server.
114
+
102
115
  ## Roadmap
103
116
 
104
117
  * [ ] Add support for multiple themes
105
- * [ ] Backend integration choice — switch between PHP, Python, or Node
118
+ * [x] Backend integration choice — pick **PHP or Node.js** at creation (Python planned)
119
+ * [ ] Extend documentation with advanced usage examples
@@ -34,7 +34,7 @@ function addPageBlock(pageName) {
34
34
 
35
35
  const block =
36
36
  `{% elif title == "${camelName}" %}\n` +
37
- `{#{% include "component.njk" %}#}\n\n`;
37
+ `{% include "component.njk" ignore missing %}\n\n`;
38
38
 
39
39
  writePageComponents(content.replace('{% else %}', `${block}{% else %}`));
40
40
  console.log(`[updated] page block added for "${camelName}"`);
package/bin/create.js CHANGED
@@ -28,6 +28,11 @@ const FRAMEWORK = Object.freeze({
28
28
  NONE: 'none',
29
29
  });
30
30
 
31
+ const BACKEND = Object.freeze({
32
+ NODE: 'node',
33
+ PHP: 'php',
34
+ });
35
+
31
36
  // ── CHOICES ──────────────────────────────────────────────────────────────────
32
37
 
33
38
  const LANGUAGE_CHOICES = [
@@ -43,6 +48,11 @@ const FRAMEWORK_CHOICES = [
43
48
  { label: 'None', value: FRAMEWORK.NONE },
44
49
  ];
45
50
 
51
+ const BACKEND_CHOICES = [
52
+ { label: 'Node.js (index.js — no PHP, no Composer)', value: BACKEND.NODE },
53
+ { label: 'PHP (index.php — Composer dependencies)', value: BACKEND.PHP },
54
+ ];
55
+
46
56
  // ── COPY CONFIG ───────────────────────────────────────────────────────────────
47
57
 
48
58
  const MANDATORY_COPY = [
@@ -117,6 +127,9 @@ node_modules/
117
127
  src/backend/_core/vendor/
118
128
  out/
119
129
  src/backend/config.php
130
+ src/backend/config.js
131
+ src/backend/node_modules/
132
+ src/backend/cache/
120
133
  `;
121
134
 
122
135
 
@@ -203,8 +216,9 @@ function njkUncomment(content, line) {
203
216
  return content.split(`{# ${line} #}`).join(line);
204
217
  }
205
218
 
206
- function installDependencies() {
207
- const backendCore = path.join(targetDir, 'src', 'backend', '_core');
219
+ function installDependencies(backend) {
220
+ const backendRoot = path.join(targetDir, 'src', 'backend');
221
+ const backendCore = path.join(backendRoot, '_core');
208
222
 
209
223
  log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
210
224
  const npm = spawnSync('npm', ['install'], {
@@ -220,6 +234,20 @@ function installDependencies() {
220
234
  return false;
221
235
  }
222
236
 
237
+ // --- Node backend: install its own deps (mysql2), then SKIP Composer ---
238
+ if (backend === BACKEND.NODE) {
239
+ if (fs.existsSync(path.join(backendRoot, 'package.json'))) {
240
+ log(`\n${color.blue}>> Installing Node backend modules...${color.reset}`);
241
+ spawnSync('npm', ['install'], {
242
+ cwd: backendRoot,
243
+ stdio: 'inherit',
244
+ shell: process.platform === 'win32',
245
+ });
246
+ }
247
+ return true; // Composer is never run for the Node backend
248
+ }
249
+
250
+ // --- PHP backend: install Composer dependencies (if present) ---
223
251
  if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
224
252
  return true;
225
253
  }
@@ -353,6 +381,7 @@ async function init() {
353
381
 
354
382
  const language = await askChoice('Select a language', LANGUAGE_CHOICES);
355
383
  const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
384
+ const backend = await askChoice('Select a backend', BACKEND_CHOICES);
356
385
 
357
386
  log('');
358
387
 
@@ -372,6 +401,13 @@ async function init() {
372
401
  logAdd('src/backend/config.php');
373
402
  }
374
403
 
404
+ const configJsDest = path.join(targetDir, 'src/backend/config.js');
405
+ const configJsExample = path.join(targetDir, 'src/backend/example.config.js');
406
+ if (!fs.existsSync(configJsDest) && fs.existsSync(configJsExample)) {
407
+ fs.copyFileSync(configJsExample, configJsDest);
408
+ logAdd('src/backend/config.js');
409
+ }
410
+
375
411
  const pkg = { ...PROJECT_PACKAGE };
376
412
 
377
413
  if (language === LANGUAGE.TYPESCRIPT) {
@@ -395,7 +431,7 @@ async function init() {
395
431
  applyFramework(framework);
396
432
  applyLanguage(language);
397
433
 
398
- installDependencies();
434
+ installDependencies(backend);
399
435
 
400
436
  log(`\n${color.green}>> Done!${color.reset}`);
401
437
  log(`${color.yellow}\nNow run:\n${color.reset}`);
package/docs/Backend.md CHANGED
@@ -1,46 +1,90 @@
1
1
  # Backend
2
2
 
3
- > **PHP requirement** — the backend runs only on servers with PHP support:
4
- > - **Apache** (shared hosting, VPS) ✅
5
- > - **Nginx** + PHP-FPM (VPS) ✅
6
- > - **IIS** + FastCGI (Windows Server) ✅
7
- > - **Static hosting** (Netlify, Vercel, GitHub Pages, Cloudflare Pages) ❌
8
-
9
- The backend is a PHP REST API located in `src/backend/`, copied to the output directory automatically at build time.
3
+ Nibula ships **two interchangeable backends** in `src/backend/` — a **PHP** one
4
+ and a **Node.js** one. You pick which to use when you scaffold a project
5
+ (`nib new`). Both provide the same REST API: same routing, same `Response`
6
+ envelope, same `X-Api-Key` auth, same CORS model, same file-based rate limiter.
7
+ Only the language differs.
8
+
9
+ > **Server requirement** — where each backend can run:
10
+ >
11
+ > | Host | PHP | Node.js |
12
+ > |---|---|---|
13
+ > | Apache (shared hosting, VPS) | ✅ | ⚠ïļ needs a persistent process (VPS only) |
14
+ > | Nginx + PHP-FPM (VPS) | ✅ | ✅ (Nginx reverse-proxies to Node) |
15
+ > | IIS (Windows Server) | ✅ FastCGI | ✅ via ARR reverse proxy |
16
+ > | Static hosting (Netlify, Vercel, GitHub Pages, Cloudflare Pages) | ❌ | ❌ |
17
+ >
18
+ > Typical shared hosting can run **PHP** but not a long-running Node process, so
19
+ > choose PHP there. On a VPS you control, either works.
20
+
21
+ The backend lives in `src/backend/` and is copied to the output directory
22
+ automatically at build time.
23
+
24
+ ## Choosing a backend
25
+
26
+ When you run `nib new`, the scaffolder asks three questions: language, CSS
27
+ framework, and **backend**. Pick Node.js or PHP.
28
+
29
+ - **If you pick Node**, Composer is never run — you get an npm-only setup, and
30
+ the backend's own dependency (`mysql2`, used only if you touch the database)
31
+ is installed for you.
32
+ - **If you pick PHP**, Composer dependencies are installed as before.
33
+
34
+ Both file sets are always present in `src/backend/`, so you can switch later
35
+ without re-scaffolding. The PHP front controller only looks at `.php` endpoint
36
+ files and the Node one only at `.js` files, so they coexist without conflict.
10
37
 
11
38
  ## Structure
12
39
 
13
40
  ```
14
41
  src/backend/
15
42
  ├── api/
16
- │ └── protected/ # Endpoints requiring X-Api-Key header
17
43
  │ ├── public/ # Endpoints accessible without an API key
44
+ │ └── protected/ # Endpoints requiring the X-Api-Key header
18
45
  ├── _core/ # Framework internals (routing, modules) — do not edit
19
- │ └── modules/ # Response, RateLimiter, ...
46
+ │ ├── index.php # PHP front controller
47
+ │ ├── index.js # Node front controller (also the HTTP server)
48
+ │ ├── init.php / init.js
49
+ │ └── modules/ # Response, RateLimiter (.php and .js)
20
50
  ├── database/
21
- │ ├── Database.php
51
+ │ ├── Database.php # PDO singleton
52
+ │ ├── Database.js # mysql2 pool singleton
22
53
  │ └── migrations/
23
- ├── example.config.php # Template — versioned, safe to commit
24
- └── config.php # Your local config, generated on setup — never commit this
54
+ ├── package.json # Node backend deps + start script
55
+ ├── example.config.php # PHP template — versioned, safe to commit
56
+ ├── example.config.js # Node template — versioned, safe to commit
57
+ ├── config.php # Local PHP config — generated on setup, never commit
58
+ └── config.js # Local Node config — generated on setup, never commit
25
59
  ```
26
60
 
27
- > A scaffolded project contains **both** files. `config.php` is generated automatically by `nib new` (a copy of the example) and is git-ignored, so your secrets stay local and are never pushed. `example.config.php` is kept and versioned: it's the file that ends up on GitHub, acting as a secret-free reference so anyone cloning the project knows which keys to set. If `config.php` is ever missing (e.g. after a fresh clone), just copy `example.config.php` to `config.php`.
61
+ > A scaffolded project contains **both** the example and the generated config for
62
+ > each backend. `config.php` / `config.js` are generated automatically by
63
+ > `nib new` (copies of the examples) and are git-ignored, so your secrets stay
64
+ > local. The `example.config.*` files are versioned: they end up on GitHub as
65
+ > secret-free references. If a `config.*` is ever missing (e.g. after a fresh
66
+ > clone), just copy the matching `example.config.*` to it.
28
67
 
29
- > Note: in the boilerplate repo itself, only `example.config.php` exists — `config.php` is created at scaffold time, not shipped.
68
+ ## Key difference: how each backend runs
30
69
 
31
- ## Configuration
70
+ - **PHP** is executed **per request** by the web server through PHP-FPM (or
71
+ FastCGI/mod_php). There is no process to keep alive.
72
+ - **Node** is a **long-running process** that *is* the server, listening on
73
+ `127.0.0.1:3000`. The web server (Nginx/Apache/IIS) **reverse-proxies** `/api`
74
+ to it. You start it once and keep it alive (systemd/pm2). See `docs/Deploy.md`.
32
75
 
33
- `config.php` works like a `.env` file — it holds secrets and environment settings that stay local and out of version control.
76
+ ## Configuration
34
77
 
35
- Fill in your values:
78
+ `config.php` / `config.js` work like a `.env` file — they hold secrets and
79
+ environment settings that stay local and out of version control. Same keys in
80
+ both, just PHP-array vs JS-object syntax.
36
81
 
37
82
  ### config.php <small>(`src/backend/`)</small>
38
83
  ```php
39
- // Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
84
+ // Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
40
85
  'GENERAL_API_KEY' => 'DEFAULT_KEY',
41
86
 
42
- // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
43
- // For subfolder endpoints, use the relative path ('subfolder/endpoint')
87
+ // Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
44
88
  'CUSTOM_ENDPOINT_KEYS' => [
45
89
  'subfolder/endpoint' => 'custom-key',
46
90
  ],
@@ -61,39 +105,100 @@ Fill in your values:
61
105
  'DB_PASS' => '',
62
106
  ```
63
107
 
108
+ ### config.js <small>(`src/backend/`)</small>
109
+ ```js
110
+ module.exports = {
111
+ GENERAL_API_KEY: 'DEFAULT_KEY',
112
+ CUSTOM_ENDPOINT_KEYS: {
113
+ 'subfolder/endpoint': 'custom-key',
114
+ },
115
+ GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
116
+ CUSTOM_ENDPOINT_ORIGINS: {
117
+ 'subfolder/endpoint': ['https://app.example.com'],
118
+ },
119
+ DB_HOST: '127.0.0.1',
120
+ DB_NAME: 'example_db',
121
+ DB_USER: 'root',
122
+ DB_PASS: '',
123
+ APP_ENV: 'production', // anything other than 'production' = verbose errors
124
+ };
125
+ ```
126
+
64
127
  ### API keys
65
128
 
66
- `GENERAL_API_KEY` is the fallback key for all protected endpoints. Use `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.
129
+ `GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
130
+ `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for
131
+ subfolder endpoints, use the relative path as the key.
67
132
 
68
- > ⚠ïļ The API key travels in the `X-Api-Key` header on every request. Use it only for server-to-server calls over HTTPS. Never embed it in frontend code, where it would be publicly visible.
133
+ > ⚠ïļ The API key travels in the `X-Api-Key` header on every request. Use it only
134
+ > for server-to-server calls over HTTPS. Never embed it in frontend code, where
135
+ > it would be publicly visible.
69
136
 
70
137
  ### CORS (allowed origins)
71
138
 
72
139
  Cross-origin access mirrors the API-key model:
73
140
 
74
- - `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any endpoint from a browser.
75
- - `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint, keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
141
+ - `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any
142
+ endpoint from a browser.
143
+ - `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint,
144
+ keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
76
145
 
77
- Origins must be exact (scheme + host, no trailing slash), e.g. `https://example.com`. When a request's `Origin` is in the resolved list, it is reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty list sends no CORS header — the most restrictive setting; same-origin requests still work. Use `'*'` as the only element to allow any origin (not recommended for protected endpoints).
146
+ Origins must be exact (scheme + host, no trailing slash), e.g.
147
+ `https://example.com`. When a request's `Origin` is in the resolved list, it is
148
+ reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty
149
+ list sends no CORS header — the most restrictive setting; same-origin requests
150
+ still work. Use `'*'` as the only element to allow any origin (not recommended
151
+ for protected endpoints).
78
152
 
79
- Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise `GENERAL_ALLOWED_ORIGINS`.
153
+ Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
154
+ present, otherwise `GENERAL_ALLOWED_ORIGINS`.
80
155
 
81
156
  ## How routing works
82
157
 
83
- The file path inside `api/` maps directly to the URL. Extra URL segments become route parameters available as `$requestParams[]`.
158
+ The file path inside `api/` maps directly to the URL. The `public`/`protected`
159
+ folder does **not** appear in the URL — it only decides whether the `X-Api-Key`
160
+ check applies. So `api/public/example-public` and `api/protected/example-protected`
161
+ are both reached at `/api/<endpoint>`:
162
+
163
+ - `api/public/example-public` → `/api/example-public` (no key)
164
+ - `api/protected/example-protected` → `/api/example-protected` (requires key)
84
165
 
85
- Every endpoint file has access to:
166
+ Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
167
+ segments become route parameters. Every endpoint has access to:
86
168
 
87
- | Variable | Description |
169
+ | Variable / field | Description |
88
170
  |---|---|
89
- | `$method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
90
- | `$requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
171
+ | `method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
172
+ | `requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
173
+
174
+ > If two files share the same path — one in `public/`, one in `protected/` — the
175
+ > public one wins (it is checked first), so the endpoint ends up **without**
176
+ > authentication. Don't duplicate names across the two folders.
177
+
178
+ > **About the examples below** — the code samples use **JavaScript (the Node
179
+ > backend)** as the reference. That's just the example language: if you chose the
180
+ > **PHP** backend it works exactly the same way, with the same variables, the
181
+ > same `Response` helper and the same behaviour — only the syntax differs. Each
182
+ > section shows the JS version first, with the equivalent PHP right after.
91
183
 
92
184
  ## Creating a public endpoint
93
185
 
94
- Create a `.php` file anywhere inside `api/public/`
186
+ Create a file anywhere inside `api/public/`.
187
+
188
+ ### `api/public/example.js` (Node — reference)
189
+ ```js
190
+ module.exports = ({ method, requestParams, Response }) => {
191
+ if (method !== 'GET') Response.error('Method not allowed', 405);
192
+ const id = requestParams[0] ? parseInt(requestParams[0], 10) : null;
193
+ Response.success({ id });
194
+ };
195
+ ```
196
+
197
+ The Node handler receives a context object: `method`, `requestParams`,
198
+ `Response` (`.success` / `.error` / `.noContent`), plus `query`, `body` (parsed
199
+ JSON), `rawBody`, `headers`, `config`, `req`, `res`.
95
200
 
96
- ### api/public/example.php
201
+ The same endpoint in PHP — identical logic, PHP syntax:
97
202
  ```php
98
203
  <?php
99
204
  declare(strict_types=1);
@@ -111,9 +216,18 @@ Response::success(['id' => $id]);
111
216
 
112
217
  ## Creating a protected endpoint
113
218
 
114
- Create a `.php` file inside `api/protected/`. The API key check happens automatically before your file runs.
219
+ Create a file inside `api/protected/`. The API-key check happens automatically
220
+ before your file runs.
115
221
 
116
- ### api/protected/example.php
222
+ ### `api/protected/example.js` (Node — reference)
223
+ ```js
224
+ module.exports = ({ method, Response }) => {
225
+ if (method !== 'GET') Response.error('Method not allowed', 405);
226
+ Response.success({ visits: 1024 });
227
+ };
228
+ ```
229
+
230
+ The same endpoint in PHP:
117
231
  ```php
118
232
  <?php
119
233
  declare(strict_types=1);
@@ -127,25 +241,48 @@ if ($method !== 'GET') {
127
241
  Response::success(['visits' => 1024]);
128
242
  ```
129
243
 
130
- To assign a dedicated key, add it to `config.php`:
244
+ To assign a dedicated key, add it to your config:
131
245
 
246
+ ```js
247
+ CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
248
+ ```
132
249
  ```php
133
- 'CUSTOM_ENDPOINT_KEYS' => [
134
- 'endpoint' => 'custom-key',
135
- ],
250
+ 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
136
251
  ```
137
252
 
138
253
  ## The Response helper
139
254
 
255
+ Identical envelope in both backends — JS shown as reference, PHP works the same:
256
+
257
+ ```js
258
+ Response.success(data, code); // default 200
259
+ Response.error(message, code, details); // default 400
260
+ Response.noContent(); // 204
261
+ ```
140
262
  ```php
141
263
  Response::success($data, $code); // default 200
142
264
  Response::error($message, $code, $details); // default 400
143
- Response::noContent(); // 204
265
+ Response::noContent(); // 204
144
266
  ```
145
267
 
268
+ Response shapes:
269
+ - success → `{ "status": "success", "data": ... }`
270
+ - error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
271
+
272
+ ## Database
273
+
274
+ Both backends expose a connection singleton reading the same config.
275
+
276
+ - **Node** (reference) — `database/Database.js`, a `mysql2` pool singleton:
277
+ `const pool = Database.getInstance(); const [rows] = await pool.execute(sql, params);`
278
+ The `mysql2` driver is installed at scaffold time when you choose Node (it is
279
+ loaded lazily, so the backend also boots fine without a database).
280
+ - **PHP** — `database/Database.php`, a PDO singleton:
281
+ `$pdo = Database::getInstance();`
282
+
146
283
  ## Pre-built endpoints
147
284
 
148
285
  | Route | Method | Description |
149
286
  |---|---|---|
150
- | `/api/public/example-public` | `GET` | Example endpoint that doesn't require any key |
151
- | `/api/protected/example-protected` | `GET` | Example endpoint that requires X-API-KEY |
287
+ | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
288
+ | `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |