nibula 1.2.4 → 1.2.6
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/.eleventy.js +81 -81
- package/.eleventyignore +3 -3
- package/.github/workflows/publish.yml +37 -0
- package/CHANGELOG.md +179 -148
- package/LICENSE +169 -169
- package/NOTICE +4 -4
- package/README.md +120 -123
- package/bin/create.js +507 -507
- package/bin/nibula.js +317 -305
- package/docs/Assistant CLI.md +65 -65
- package/docs/Backend.md +295 -295
- package/docs/Components.md +84 -84
- package/docs/Creating pages.md +64 -64
- package/docs/Deploy.md +189 -189
- package/docs/Head and SEO.md +138 -138
- package/docs/Javascript.md +50 -50
- package/docs/Styling with SCSS.md +141 -141
- package/nginx.conf +75 -75
- package/package.json +1 -1
- package/src/backend/.htaccess +6 -6
- package/src/backend/_core/composer.json +5 -5
- package/src/backend/_core/composer.lock +492 -492
- package/src/backend/_core/index.js +267 -267
- package/src/backend/_core/index.php +147 -147
- package/src/backend/_core/init.js +52 -52
- package/src/backend/_core/init.php +33 -33
- package/src/backend/_core/modules/RateLimiter.js +58 -58
- package/src/backend/_core/modules/RateLimiter.php +30 -30
- package/src/backend/_core/modules/Response.js +59 -59
- package/src/backend/_core/modules/Response.php +48 -48
- package/src/backend/api/protected/example-protected.js +23 -23
- package/src/backend/api/protected/example-protected.php +16 -16
- package/src/backend/api/public/example-public.js +24 -24
- package/src/backend/api/public/example-public.php +16 -16
- package/src/backend/backend-node.service.example +30 -30
- package/src/backend/database/Database.js +46 -46
- package/src/backend/database/Database.php +23 -23
- package/src/backend/example.config.js +37 -37
- package/src/backend/example.config.php +27 -27
- package/src/backend/package.json +18 -18
- package/src/backend/web.config +16 -16
- package/src/frontend/.htaccess +51 -51
- package/src/frontend/404.njk +17 -17
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/components/global/footer.njk +25 -25
- package/src/frontend/components/global/header.njk +6 -6
- package/src/frontend/components/welcome.njk +114 -114
- package/src/frontend/data/site.json +48 -48
- package/src/frontend/index.njk +8 -8
- package/src/frontend/js/modules/exampleModule.js +2 -2
- package/src/frontend/js/pages/404.js +6 -6
- package/src/frontend/js/pages/homepage.js +6 -6
- package/src/frontend/layouts/base.njk +132 -132
- package/src/frontend/layouts/page-components.njk +13 -13
- package/src/frontend/llms.njk +17 -17
- package/src/frontend/robots.njk +7 -7
- package/src/frontend/scss/modules/_animations.scss +24 -24
- package/src/frontend/scss/modules/_footer.scss +27 -27
- package/src/frontend/scss/modules/_global.scss +47 -47
- package/src/frontend/scss/modules/_header.scss +27 -27
- package/src/frontend/scss/modules/_mobile.scss +29 -29
- package/src/frontend/scss/modules/_root.scss +34 -34
- package/src/frontend/scss/modules/_typography.scss +14 -14
- package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
- package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
- package/src/frontend/scss/modules/frameworks/_foundation.scss +138 -139
- package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
- package/src/frontend/scss/pages/404.scss +27 -27
- package/src/frontend/scss/pages/homepage.scss +22 -22
- package/src/frontend/sitemap.njk +17 -17
- package/src/frontend/ts/modules/exampleModule.ts +2 -2
- package/src/frontend/ts/pages/404.ts +6 -6
- package/src/frontend/ts/pages/homepage.ts +6 -6
- package/src/frontend/web.config +55 -55
- package/tools/config/messages.json +3 -1
- package/tools/res/templates/template.js +6 -6
- package/tools/res/templates/template.njk +8 -8
- package/tools/res/templates/template.scss +22 -22
- package/tools/res/templates/template.ts +6 -6
- package/tsconfig.json +24 -24
package/docs/Backend.md
CHANGED
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
# Backend
|
|
2
|
-
|
|
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 dependency (`mysql2`, used only if you touch the database) is
|
|
31
|
-
added to the **root** `package.json` and installed into the root `node_modules`.
|
|
32
|
-
- **If you pick PHP**, Composer dependencies are installed as before, and no Node
|
|
33
|
-
backend files are added.
|
|
34
|
-
|
|
35
|
-
**Only the backend you choose is copied into the project.** A Node project
|
|
36
|
-
contains no PHP files; a PHP project contains no `.js` backend files. Shared,
|
|
37
|
-
backend-agnostic files (SQL migrations, `.htaccess`, `web.config`) are always
|
|
38
|
-
included.
|
|
39
|
-
|
|
40
|
-
## Structure
|
|
41
|
-
|
|
42
|
-
The structure below shows both backends for reference; **your project will have
|
|
43
|
-
only one side of each pair** (plus the shared files).
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
src/backend/
|
|
47
|
-
├── api/
|
|
48
|
-
│ ├── public/ # Endpoints accessible without an API key
|
|
49
|
-
│ └── protected/ # Endpoints requiring the X-Api-Key header
|
|
50
|
-
├── _core/ # Framework internals (routing, modules) — do not edit
|
|
51
|
-
│ ├── index.php # PHP front controller (PHP project)
|
|
52
|
-
│ ├── index.js # Node front controller + server (Node project)
|
|
53
|
-
│ ├── init.php / init.js
|
|
54
|
-
│ └── modules/ # Response, RateLimiter
|
|
55
|
-
├── database/
|
|
56
|
-
│ ├── Database.php # PDO singleton (PHP project)
|
|
57
|
-
│ ├── Database.js # mysql2 pool singleton (Node project)
|
|
58
|
-
│ └── migrations/ # shared
|
|
59
|
-
├── package.json # Node project only — backend deps + start script
|
|
60
|
-
├── example.config.php # PHP project only — versioned template
|
|
61
|
-
├── example.config.js # Node project only — versioned template
|
|
62
|
-
├── config.php # PHP project only — generated on setup, never commit
|
|
63
|
-
└── config.js # Node project only — generated on setup, never commit
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
> `config.php` / `config.js` are generated automatically by `nib new` (a copy of
|
|
67
|
-
> the matching `example.config.*`) and are git-ignored, so your secrets stay
|
|
68
|
-
> local. The `example.config.*` file is versioned as a secret-free reference. If
|
|
69
|
-
> `config.*` is ever missing (e.g. after a fresh clone), copy the example to it.
|
|
70
|
-
|
|
71
|
-
> **Where do the Node backend's dependencies live?** In the **root**
|
|
72
|
-
> `node_modules`, not in `src/backend` — so your source tree stays clean (no
|
|
73
|
-
> `src/backend/node_modules` locally). On the server you install them into
|
|
74
|
-
> `out/backend` instead; see `docs/Deploy.md`.
|
|
75
|
-
|
|
76
|
-
## Key difference: how each backend runs
|
|
77
|
-
|
|
78
|
-
- **PHP** is executed **per request** by the web server through PHP-FPM (or
|
|
79
|
-
FastCGI/mod_php). There is no process to keep alive.
|
|
80
|
-
- **Node** is a **long-running process** that *is* the server, listening on
|
|
81
|
-
`127.0.0.1:3000`. The web server (Nginx/Apache/IIS) **reverse-proxies** `/api`
|
|
82
|
-
to it. You start it once and keep it alive (systemd/pm2). See `docs/Deploy.md`.
|
|
83
|
-
|
|
84
|
-
## Configuration
|
|
85
|
-
|
|
86
|
-
`config.php` / `config.js` work like a `.env` file — they hold secrets and
|
|
87
|
-
environment settings that stay local and out of version control. Same keys in
|
|
88
|
-
both, just PHP-array vs JS-object syntax.
|
|
89
|
-
|
|
90
|
-
### config.php <small>(`src/backend/`)</small>
|
|
91
|
-
```php
|
|
92
|
-
// Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
|
|
93
|
-
'GENERAL_API_KEY' => 'DEFAULT_KEY',
|
|
94
|
-
|
|
95
|
-
// Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
|
|
96
|
-
'CUSTOM_ENDPOINT_KEYS' => [
|
|
97
|
-
'subfolder/endpoint' => 'custom-key',
|
|
98
|
-
],
|
|
99
|
-
|
|
100
|
-
'GENERAL_ALLOWED_ORIGINS' => [
|
|
101
|
-
'*',
|
|
102
|
-
// 'https://example.com',
|
|
103
|
-
],
|
|
104
|
-
|
|
105
|
-
'CUSTOM_ENDPOINT_ORIGINS' => [
|
|
106
|
-
'subfolder/endpoint' => ['https://app.example.com'],
|
|
107
|
-
],
|
|
108
|
-
|
|
109
|
-
// Database configuration
|
|
110
|
-
'DB_HOST' => '127.0.0.1',
|
|
111
|
-
'DB_NAME' => 'example_db',
|
|
112
|
-
'DB_USER' => 'root',
|
|
113
|
-
'DB_PASS' => '',
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### config.js <small>(`src/backend/`)</small>
|
|
117
|
-
```js
|
|
118
|
-
module.exports = {
|
|
119
|
-
GENERAL_API_KEY: 'DEFAULT_KEY',
|
|
120
|
-
CUSTOM_ENDPOINT_KEYS: {
|
|
121
|
-
'subfolder/endpoint': 'custom-key',
|
|
122
|
-
},
|
|
123
|
-
GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
|
|
124
|
-
CUSTOM_ENDPOINT_ORIGINS: {
|
|
125
|
-
'subfolder/endpoint': ['https://app.example.com'],
|
|
126
|
-
},
|
|
127
|
-
DB_HOST: '127.0.0.1',
|
|
128
|
-
DB_NAME: 'example_db',
|
|
129
|
-
DB_USER: 'root',
|
|
130
|
-
DB_PASS: '',
|
|
131
|
-
APP_ENV: 'production', // anything other than 'production' = verbose errors
|
|
132
|
-
};
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### API keys
|
|
136
|
-
|
|
137
|
-
`GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
|
|
138
|
-
`CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for
|
|
139
|
-
subfolder endpoints, use the relative path as the key.
|
|
140
|
-
|
|
141
|
-
> ⚠️ The API key travels in the `X-Api-Key` header on every request. Use it only
|
|
142
|
-
> for server-to-server calls over HTTPS. Never embed it in frontend code, where
|
|
143
|
-
> it would be publicly visible.
|
|
144
|
-
|
|
145
|
-
### CORS (allowed origins)
|
|
146
|
-
|
|
147
|
-
Cross-origin access mirrors the API-key model:
|
|
148
|
-
|
|
149
|
-
- `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any
|
|
150
|
-
endpoint from a browser.
|
|
151
|
-
- `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint,
|
|
152
|
-
keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
|
|
153
|
-
|
|
154
|
-
Origins must be exact (scheme + host, no trailing slash), e.g.
|
|
155
|
-
`https://example.com`. When a request's `Origin` is in the resolved list, it is
|
|
156
|
-
reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty
|
|
157
|
-
list sends no CORS header — the most restrictive setting; same-origin requests
|
|
158
|
-
still work. Use `'*'` as the only element to allow any origin (not recommended
|
|
159
|
-
for protected endpoints).
|
|
160
|
-
|
|
161
|
-
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
|
|
162
|
-
present, otherwise `GENERAL_ALLOWED_ORIGINS`.
|
|
163
|
-
|
|
164
|
-
## How routing works
|
|
165
|
-
|
|
166
|
-
The file path inside `api/` maps directly to the URL. The `public`/`protected`
|
|
167
|
-
folder does **not** appear in the URL — it only decides whether the `X-Api-Key`
|
|
168
|
-
check applies. So `api/public/example-public` and `api/protected/example-protected`
|
|
169
|
-
are both reached at `/api/<endpoint>`:
|
|
170
|
-
|
|
171
|
-
- `api/public/example-public` → `/api/example-public` (no key)
|
|
172
|
-
- `api/protected/example-protected` → `/api/example-protected` (requires key)
|
|
173
|
-
|
|
174
|
-
Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
|
|
175
|
-
segments become route parameters. Every endpoint has access to:
|
|
176
|
-
|
|
177
|
-
| Variable / field | Description |
|
|
178
|
-
|---|---|
|
|
179
|
-
| `method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
|
|
180
|
-
| `requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
|
|
181
|
-
|
|
182
|
-
> If two files share the same path — one in `public/`, one in `protected/` — the
|
|
183
|
-
> public one wins (it is checked first), so the endpoint ends up **without**
|
|
184
|
-
> authentication. Don't duplicate names across the two folders.
|
|
185
|
-
|
|
186
|
-
> **About the examples below** — the code samples use **JavaScript (the Node
|
|
187
|
-
> backend)** as the reference. That's just the example language: if you chose the
|
|
188
|
-
> **PHP** backend it works exactly the same way, with the same variables, the
|
|
189
|
-
> same `Response` helper and the same behaviour — only the syntax differs. Each
|
|
190
|
-
> section shows the JS version first, with the equivalent PHP right after.
|
|
191
|
-
|
|
192
|
-
## Creating a public endpoint
|
|
193
|
-
|
|
194
|
-
Create a file anywhere inside `api/public/`.
|
|
195
|
-
|
|
196
|
-
### `api/public/example.js` (Node — reference)
|
|
197
|
-
```js
|
|
198
|
-
module.exports = ({ method, requestParams, Response }) => {
|
|
199
|
-
if (method !== 'GET') Response.error('Method not allowed', 405);
|
|
200
|
-
const id = requestParams[0] ? parseInt(requestParams[0], 10) : null;
|
|
201
|
-
Response.success({ id });
|
|
202
|
-
};
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The Node handler receives a context object: `method`, `requestParams`,
|
|
206
|
-
`Response` (`.success` / `.error` / `.noContent`), plus `query`, `body` (parsed
|
|
207
|
-
JSON), `rawBody`, `headers`, `config`, `req`, `res`.
|
|
208
|
-
|
|
209
|
-
The same endpoint in PHP — identical logic, PHP syntax:
|
|
210
|
-
```php
|
|
211
|
-
<?php
|
|
212
|
-
declare(strict_types=1);
|
|
213
|
-
|
|
214
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
215
|
-
|
|
216
|
-
if ($method !== 'GET') {
|
|
217
|
-
Response::error('Method not allowed', 405);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
221
|
-
|
|
222
|
-
Response::success(['id' => $id]);
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Creating a protected endpoint
|
|
226
|
-
|
|
227
|
-
Create a file inside `api/protected/`. The API-key check happens automatically
|
|
228
|
-
before your file runs.
|
|
229
|
-
|
|
230
|
-
### `api/protected/example.js` (Node — reference)
|
|
231
|
-
```js
|
|
232
|
-
module.exports = ({ method, Response }) => {
|
|
233
|
-
if (method !== 'GET') Response.error('Method not allowed', 405);
|
|
234
|
-
Response.success({ visits: 1024 });
|
|
235
|
-
};
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
The same endpoint in PHP:
|
|
239
|
-
```php
|
|
240
|
-
<?php
|
|
241
|
-
declare(strict_types=1);
|
|
242
|
-
|
|
243
|
-
require_once CORE_PATH . '/modules/Response.php';
|
|
244
|
-
|
|
245
|
-
if ($method !== 'GET') {
|
|
246
|
-
Response::error('Method not allowed', 405);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
Response::success(['visits' => 1024]);
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
To assign a dedicated key, add it to your config:
|
|
253
|
-
|
|
254
|
-
```js
|
|
255
|
-
CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
|
|
256
|
-
```
|
|
257
|
-
```php
|
|
258
|
-
'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## The Response helper
|
|
262
|
-
|
|
263
|
-
Identical envelope in both backends — JS shown as reference, PHP works the same:
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
Response.success(data, code); // default 200
|
|
267
|
-
Response.error(message, code, details); // default 400
|
|
268
|
-
Response.noContent(); // 204
|
|
269
|
-
```
|
|
270
|
-
```php
|
|
271
|
-
Response::success($data, $code); // default 200
|
|
272
|
-
Response::error($message, $code, $details); // default 400
|
|
273
|
-
Response::noContent(); // 204
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Response shapes:
|
|
277
|
-
- success → `{ "status": "success", "data": ... }`
|
|
278
|
-
- error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
|
|
279
|
-
|
|
280
|
-
## Database
|
|
281
|
-
|
|
282
|
-
Both backends expose a connection singleton reading the same config.
|
|
283
|
-
|
|
284
|
-
- **Node** (reference) — `database/Database.js`, a `mysql2` pool singleton:
|
|
285
|
-
`const pool = Database.getInstance(); const [rows] = await pool.execute(sql, params);`
|
|
286
|
-
The `mysql2` driver is installed at scaffold time when you choose Node (it is
|
|
287
|
-
loaded lazily, so the backend also boots fine without a database).
|
|
288
|
-
- **PHP** — `database/Database.php`, a PDO singleton:
|
|
289
|
-
`$pdo = Database::getInstance();`
|
|
290
|
-
|
|
291
|
-
## Pre-built endpoints
|
|
292
|
-
|
|
293
|
-
| Route | Method | Description |
|
|
294
|
-
|---|---|---|
|
|
295
|
-
| `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
|
|
1
|
+
# Backend
|
|
2
|
+
|
|
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 dependency (`mysql2`, used only if you touch the database) is
|
|
31
|
+
added to the **root** `package.json` and installed into the root `node_modules`.
|
|
32
|
+
- **If you pick PHP**, Composer dependencies are installed as before, and no Node
|
|
33
|
+
backend files are added.
|
|
34
|
+
|
|
35
|
+
**Only the backend you choose is copied into the project.** A Node project
|
|
36
|
+
contains no PHP files; a PHP project contains no `.js` backend files. Shared,
|
|
37
|
+
backend-agnostic files (SQL migrations, `.htaccess`, `web.config`) are always
|
|
38
|
+
included.
|
|
39
|
+
|
|
40
|
+
## Structure
|
|
41
|
+
|
|
42
|
+
The structure below shows both backends for reference; **your project will have
|
|
43
|
+
only one side of each pair** (plus the shared files).
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
src/backend/
|
|
47
|
+
├── api/
|
|
48
|
+
│ ├── public/ # Endpoints accessible without an API key
|
|
49
|
+
│ └── protected/ # Endpoints requiring the X-Api-Key header
|
|
50
|
+
├── _core/ # Framework internals (routing, modules) — do not edit
|
|
51
|
+
│ ├── index.php # PHP front controller (PHP project)
|
|
52
|
+
│ ├── index.js # Node front controller + server (Node project)
|
|
53
|
+
│ ├── init.php / init.js
|
|
54
|
+
│ └── modules/ # Response, RateLimiter
|
|
55
|
+
├── database/
|
|
56
|
+
│ ├── Database.php # PDO singleton (PHP project)
|
|
57
|
+
│ ├── Database.js # mysql2 pool singleton (Node project)
|
|
58
|
+
│ └── migrations/ # shared
|
|
59
|
+
├── package.json # Node project only — backend deps + start script
|
|
60
|
+
├── example.config.php # PHP project only — versioned template
|
|
61
|
+
├── example.config.js # Node project only — versioned template
|
|
62
|
+
├── config.php # PHP project only — generated on setup, never commit
|
|
63
|
+
└── config.js # Node project only — generated on setup, never commit
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> `config.php` / `config.js` are generated automatically by `nib new` (a copy of
|
|
67
|
+
> the matching `example.config.*`) and are git-ignored, so your secrets stay
|
|
68
|
+
> local. The `example.config.*` file is versioned as a secret-free reference. If
|
|
69
|
+
> `config.*` is ever missing (e.g. after a fresh clone), copy the example to it.
|
|
70
|
+
|
|
71
|
+
> **Where do the Node backend's dependencies live?** In the **root**
|
|
72
|
+
> `node_modules`, not in `src/backend` — so your source tree stays clean (no
|
|
73
|
+
> `src/backend/node_modules` locally). On the server you install them into
|
|
74
|
+
> `out/backend` instead; see `docs/Deploy.md`.
|
|
75
|
+
|
|
76
|
+
## Key difference: how each backend runs
|
|
77
|
+
|
|
78
|
+
- **PHP** is executed **per request** by the web server through PHP-FPM (or
|
|
79
|
+
FastCGI/mod_php). There is no process to keep alive.
|
|
80
|
+
- **Node** is a **long-running process** that *is* the server, listening on
|
|
81
|
+
`127.0.0.1:3000`. The web server (Nginx/Apache/IIS) **reverse-proxies** `/api`
|
|
82
|
+
to it. You start it once and keep it alive (systemd/pm2). See `docs/Deploy.md`.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
`config.php` / `config.js` work like a `.env` file — they hold secrets and
|
|
87
|
+
environment settings that stay local and out of version control. Same keys in
|
|
88
|
+
both, just PHP-array vs JS-object syntax.
|
|
89
|
+
|
|
90
|
+
### config.php <small>(`src/backend/`)</small>
|
|
91
|
+
```php
|
|
92
|
+
// Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
|
|
93
|
+
'GENERAL_API_KEY' => 'DEFAULT_KEY',
|
|
94
|
+
|
|
95
|
+
// Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
|
|
96
|
+
'CUSTOM_ENDPOINT_KEYS' => [
|
|
97
|
+
'subfolder/endpoint' => 'custom-key',
|
|
98
|
+
],
|
|
99
|
+
|
|
100
|
+
'GENERAL_ALLOWED_ORIGINS' => [
|
|
101
|
+
'*',
|
|
102
|
+
// 'https://example.com',
|
|
103
|
+
],
|
|
104
|
+
|
|
105
|
+
'CUSTOM_ENDPOINT_ORIGINS' => [
|
|
106
|
+
'subfolder/endpoint' => ['https://app.example.com'],
|
|
107
|
+
],
|
|
108
|
+
|
|
109
|
+
// Database configuration
|
|
110
|
+
'DB_HOST' => '127.0.0.1',
|
|
111
|
+
'DB_NAME' => 'example_db',
|
|
112
|
+
'DB_USER' => 'root',
|
|
113
|
+
'DB_PASS' => '',
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### config.js <small>(`src/backend/`)</small>
|
|
117
|
+
```js
|
|
118
|
+
module.exports = {
|
|
119
|
+
GENERAL_API_KEY: 'DEFAULT_KEY',
|
|
120
|
+
CUSTOM_ENDPOINT_KEYS: {
|
|
121
|
+
'subfolder/endpoint': 'custom-key',
|
|
122
|
+
},
|
|
123
|
+
GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
|
|
124
|
+
CUSTOM_ENDPOINT_ORIGINS: {
|
|
125
|
+
'subfolder/endpoint': ['https://app.example.com'],
|
|
126
|
+
},
|
|
127
|
+
DB_HOST: '127.0.0.1',
|
|
128
|
+
DB_NAME: 'example_db',
|
|
129
|
+
DB_USER: 'root',
|
|
130
|
+
DB_PASS: '',
|
|
131
|
+
APP_ENV: 'production', // anything other than 'production' = verbose errors
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### API keys
|
|
136
|
+
|
|
137
|
+
`GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
|
|
138
|
+
`CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for
|
|
139
|
+
subfolder endpoints, use the relative path as the key.
|
|
140
|
+
|
|
141
|
+
> ⚠️ The API key travels in the `X-Api-Key` header on every request. Use it only
|
|
142
|
+
> for server-to-server calls over HTTPS. Never embed it in frontend code, where
|
|
143
|
+
> it would be publicly visible.
|
|
144
|
+
|
|
145
|
+
### CORS (allowed origins)
|
|
146
|
+
|
|
147
|
+
Cross-origin access mirrors the API-key model:
|
|
148
|
+
|
|
149
|
+
- `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any
|
|
150
|
+
endpoint from a browser.
|
|
151
|
+
- `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint,
|
|
152
|
+
keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
|
|
153
|
+
|
|
154
|
+
Origins must be exact (scheme + host, no trailing slash), e.g.
|
|
155
|
+
`https://example.com`. When a request's `Origin` is in the resolved list, it is
|
|
156
|
+
reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty
|
|
157
|
+
list sends no CORS header — the most restrictive setting; same-origin requests
|
|
158
|
+
still work. Use `'*'` as the only element to allow any origin (not recommended
|
|
159
|
+
for protected endpoints).
|
|
160
|
+
|
|
161
|
+
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
|
|
162
|
+
present, otherwise `GENERAL_ALLOWED_ORIGINS`.
|
|
163
|
+
|
|
164
|
+
## How routing works
|
|
165
|
+
|
|
166
|
+
The file path inside `api/` maps directly to the URL. The `public`/`protected`
|
|
167
|
+
folder does **not** appear in the URL — it only decides whether the `X-Api-Key`
|
|
168
|
+
check applies. So `api/public/example-public` and `api/protected/example-protected`
|
|
169
|
+
are both reached at `/api/<endpoint>`:
|
|
170
|
+
|
|
171
|
+
- `api/public/example-public` → `/api/example-public` (no key)
|
|
172
|
+
- `api/protected/example-protected` → `/api/example-protected` (requires key)
|
|
173
|
+
|
|
174
|
+
Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
|
|
175
|
+
segments become route parameters. Every endpoint has access to:
|
|
176
|
+
|
|
177
|
+
| Variable / field | Description |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
|
|
180
|
+
| `requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
|
|
181
|
+
|
|
182
|
+
> If two files share the same path — one in `public/`, one in `protected/` — the
|
|
183
|
+
> public one wins (it is checked first), so the endpoint ends up **without**
|
|
184
|
+
> authentication. Don't duplicate names across the two folders.
|
|
185
|
+
|
|
186
|
+
> **About the examples below** — the code samples use **JavaScript (the Node
|
|
187
|
+
> backend)** as the reference. That's just the example language: if you chose the
|
|
188
|
+
> **PHP** backend it works exactly the same way, with the same variables, the
|
|
189
|
+
> same `Response` helper and the same behaviour — only the syntax differs. Each
|
|
190
|
+
> section shows the JS version first, with the equivalent PHP right after.
|
|
191
|
+
|
|
192
|
+
## Creating a public endpoint
|
|
193
|
+
|
|
194
|
+
Create a file anywhere inside `api/public/`.
|
|
195
|
+
|
|
196
|
+
### `api/public/example.js` (Node — reference)
|
|
197
|
+
```js
|
|
198
|
+
module.exports = ({ method, requestParams, Response }) => {
|
|
199
|
+
if (method !== 'GET') Response.error('Method not allowed', 405);
|
|
200
|
+
const id = requestParams[0] ? parseInt(requestParams[0], 10) : null;
|
|
201
|
+
Response.success({ id });
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The Node handler receives a context object: `method`, `requestParams`,
|
|
206
|
+
`Response` (`.success` / `.error` / `.noContent`), plus `query`, `body` (parsed
|
|
207
|
+
JSON), `rawBody`, `headers`, `config`, `req`, `res`.
|
|
208
|
+
|
|
209
|
+
The same endpoint in PHP — identical logic, PHP syntax:
|
|
210
|
+
```php
|
|
211
|
+
<?php
|
|
212
|
+
declare(strict_types=1);
|
|
213
|
+
|
|
214
|
+
require_once CORE_PATH . '/modules/Response.php';
|
|
215
|
+
|
|
216
|
+
if ($method !== 'GET') {
|
|
217
|
+
Response::error('Method not allowed', 405);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
|
|
221
|
+
|
|
222
|
+
Response::success(['id' => $id]);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Creating a protected endpoint
|
|
226
|
+
|
|
227
|
+
Create a file inside `api/protected/`. The API-key check happens automatically
|
|
228
|
+
before your file runs.
|
|
229
|
+
|
|
230
|
+
### `api/protected/example.js` (Node — reference)
|
|
231
|
+
```js
|
|
232
|
+
module.exports = ({ method, Response }) => {
|
|
233
|
+
if (method !== 'GET') Response.error('Method not allowed', 405);
|
|
234
|
+
Response.success({ visits: 1024 });
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The same endpoint in PHP:
|
|
239
|
+
```php
|
|
240
|
+
<?php
|
|
241
|
+
declare(strict_types=1);
|
|
242
|
+
|
|
243
|
+
require_once CORE_PATH . '/modules/Response.php';
|
|
244
|
+
|
|
245
|
+
if ($method !== 'GET') {
|
|
246
|
+
Response::error('Method not allowed', 405);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Response::success(['visits' => 1024]);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To assign a dedicated key, add it to your config:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
|
|
256
|
+
```
|
|
257
|
+
```php
|
|
258
|
+
'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## The Response helper
|
|
262
|
+
|
|
263
|
+
Identical envelope in both backends — JS shown as reference, PHP works the same:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
Response.success(data, code); // default 200
|
|
267
|
+
Response.error(message, code, details); // default 400
|
|
268
|
+
Response.noContent(); // 204
|
|
269
|
+
```
|
|
270
|
+
```php
|
|
271
|
+
Response::success($data, $code); // default 200
|
|
272
|
+
Response::error($message, $code, $details); // default 400
|
|
273
|
+
Response::noContent(); // 204
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Response shapes:
|
|
277
|
+
- success → `{ "status": "success", "data": ... }`
|
|
278
|
+
- error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
|
|
279
|
+
|
|
280
|
+
## Database
|
|
281
|
+
|
|
282
|
+
Both backends expose a connection singleton reading the same config.
|
|
283
|
+
|
|
284
|
+
- **Node** (reference) — `database/Database.js`, a `mysql2` pool singleton:
|
|
285
|
+
`const pool = Database.getInstance(); const [rows] = await pool.execute(sql, params);`
|
|
286
|
+
The `mysql2` driver is installed at scaffold time when you choose Node (it is
|
|
287
|
+
loaded lazily, so the backend also boots fine without a database).
|
|
288
|
+
- **PHP** — `database/Database.php`, a PDO singleton:
|
|
289
|
+
`$pdo = Database::getInstance();`
|
|
290
|
+
|
|
291
|
+
## Pre-built endpoints
|
|
292
|
+
|
|
293
|
+
| Route | Method | Description |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
|
|
296
296
|
| `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |
|