nibula 1.0.2 â 1.1.1
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/CHANGELOG.md +51 -0
- package/README.md +20 -6
- package/bin/create.js +107 -5
- package/docs/Backend.md +188 -43
- package/docs/Deploy.md +139 -50
- package/nginx.conf +37 -9
- package/package.json +1 -1
- package/src/backend/_core/index.js +267 -0
- package/src/backend/_core/init.js +52 -0
- package/src/backend/_core/modules/RateLimiter.js +58 -0
- package/src/backend/_core/modules/Response.js +59 -0
- package/src/backend/api/protected/example-protected.js +23 -0
- package/src/backend/api/public/example-public.js +24 -0
- package/src/backend/backend-node.service.example +30 -0
- package/src/backend/database/Database.js +46 -0
- package/src/backend/example.config.js +37 -0
- package/src/backend/package.json +18 -0
- package/src/frontend/.htaccess +36 -1
- package/src/frontend/assets/brand/favicon.svg +54 -54
- package/src/frontend/assets/brand/logo.svg +54 -54
- package/src/frontend/data/site.json +54 -54
- package/src/frontend/web.config +43 -15
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Nibula are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.1.0] - 2026-07-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Node.js backend** as an alternative to PHP, living side by side in
|
|
12
|
+
`src/backend/`: `_core/index.js` front controller (which is also the HTTP
|
|
13
|
+
server), `Response` and `RateLimiter` modules, a `mysql2` pool singleton
|
|
14
|
+
`Database.js`, `example.config.js`, and a `package.json` for the backend's
|
|
15
|
+
own dependencies. Same REST API as PHP: routing, `X-Api-Key` auth, CORS and
|
|
16
|
+
file-based rate limiting are identical.
|
|
17
|
+
- **Backend choice at scaffold time**: `nib new` now asks whether to use Node.js
|
|
18
|
+
or PHP, in addition to language and CSS framework.
|
|
19
|
+
- `backend/backend-node.service.example` â a ready-to-adapt **systemd** unit for
|
|
20
|
+
keeping the Node backend running on a server.
|
|
21
|
+
- `config.js` is generated from `example.config.js` at scaffold time (mirroring
|
|
22
|
+
the existing `config.php` generation).
|
|
23
|
+
- Deployment docs now cover running the Node backend with **`screen`** (quick /
|
|
24
|
+
small setups) and **systemd** (production), plus environment variables.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **Composer is now required only when the PHP backend is chosen.** If you pick
|
|
28
|
+
Node, `composer install` is skipped entirely and the backend's npm
|
|
29
|
+
dependencies (`mysql2`) are installed instead.
|
|
30
|
+
- `.htaccess`, `web.config` and `nginx.conf` now cover **both** backends. On
|
|
31
|
+
nginx, `/api` goes to Node and automatically falls back to the PHP front
|
|
32
|
+
controller when Node is unreachable. On Apache/IIS the active backend is
|
|
33
|
+
selected by configuration (documented), since per-directory configs can't
|
|
34
|
+
health-check an upstream.
|
|
35
|
+
- `.gitignore` now also ignores `src/backend/config.js`,
|
|
36
|
+
`src/backend/node_modules/` and `src/backend/cache/`.
|
|
37
|
+
- Documentation (`README.md`, `docs/Backend.md`, `docs/Deploy.md`) updated:
|
|
38
|
+
backend examples now use JavaScript/Node as the reference, with the equivalent
|
|
39
|
+
PHP shown alongside and a note that PHP behaves identically.
|
|
40
|
+
|
|
41
|
+
### Notes
|
|
42
|
+
- Both backends are always scaffolded, so you can switch later without
|
|
43
|
+
re-creating the project. The PHP front controller only serves `.php` endpoint
|
|
44
|
+
files and the Node one only `.js`, so they coexist without conflict.
|
|
45
|
+
|
|
46
|
+
## [1.0.2]
|
|
47
|
+
|
|
48
|
+
- Previous release (PHP backend only).
|
|
49
|
+
|
|
50
|
+
[1.1.0]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.1.0
|
|
51
|
+
[1.0.2]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.0.2
|
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
|
-

|
|
24
25
|

|
|
25
26
|

|
|
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.
|
|
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**:
|
|
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)
|
|
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
|
-
* [
|
|
118
|
+
* [x] Backend integration choice â pick **PHP or Node.js** at creation (Python planned)
|
|
119
|
+
* [ ] Extend documentation with advanced usage examples
|
package/bin/create.js
CHANGED
|
@@ -28,10 +28,15 @@ 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 = [
|
|
34
|
-
{ label: 'JavaScript (
|
|
39
|
+
{ label: 'JavaScript (recomended)', value: LANGUAGE.JAVASCRIPT },
|
|
35
40
|
{ label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
|
|
36
41
|
];
|
|
37
42
|
|
|
@@ -43,6 +48,17 @@ 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
|
+
|
|
56
|
+
// Runtime dependency added to the ROOT package.json when the Node backend is
|
|
57
|
+
// chosen, so it lands in the root node_modules (never in src/backend).
|
|
58
|
+
const NODE_BACKEND_DEPENDENCIES = {
|
|
59
|
+
mysql2: '^3.11.0',
|
|
60
|
+
};
|
|
61
|
+
|
|
46
62
|
// ââ COPY CONFIG âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
47
63
|
|
|
48
64
|
const MANDATORY_COPY = [
|
|
@@ -62,6 +78,14 @@ const CREATE_DIRS = [
|
|
|
62
78
|
'src/frontend/_routes',
|
|
63
79
|
];
|
|
64
80
|
|
|
81
|
+
// Backend files that belong to exactly one backend, matched by basename.
|
|
82
|
+
// Everything else (migrations, .htaccess, web.config, README, ...) is shared.
|
|
83
|
+
const NODE_ONLY_FILES = new Set(['package.json', 'backend-node.service.example']);
|
|
84
|
+
const PHP_ONLY_FILES = new Set(['composer.json', 'composer.lock']);
|
|
85
|
+
const PHP_ONLY_DIRS = new Set(['vendor']);
|
|
86
|
+
// Runtime artifacts that must never be copied from the template, either way.
|
|
87
|
+
const BACKEND_SKIP_DIRS = new Set(['node_modules', 'cache', '.git']);
|
|
88
|
+
|
|
65
89
|
// ââ FRAMEWORK CONFIG ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
66
90
|
|
|
67
91
|
const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
|
|
@@ -117,6 +141,8 @@ node_modules/
|
|
|
117
141
|
src/backend/_core/vendor/
|
|
118
142
|
out/
|
|
119
143
|
src/backend/config.php
|
|
144
|
+
src/backend/config.js
|
|
145
|
+
src/backend/cache/
|
|
120
146
|
`;
|
|
121
147
|
|
|
122
148
|
|
|
@@ -185,6 +211,48 @@ function copyRecursive(src, dest, exclude = []) {
|
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Decide whether a backend entry belongs to the chosen backend.
|
|
216
|
+
* Returns false for entries that must be SKIPPED.
|
|
217
|
+
*/
|
|
218
|
+
function backendEntryKept(basename, isDir, backend) {
|
|
219
|
+
// Never copy runtime artifacts from the template.
|
|
220
|
+
if (isDir && BACKEND_SKIP_DIRS.has(basename)) return false;
|
|
221
|
+
|
|
222
|
+
if (backend === BACKEND.NODE) {
|
|
223
|
+
// Node project: drop every PHP artifact.
|
|
224
|
+
if (basename.endsWith('.php')) return false;
|
|
225
|
+
if (PHP_ONLY_FILES.has(basename)) return false;
|
|
226
|
+
if (isDir && PHP_ONLY_DIRS.has(basename)) return false;
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// PHP project: drop every Node artifact.
|
|
231
|
+
if (basename.endsWith('.js')) return false;
|
|
232
|
+
if (NODE_ONLY_FILES.has(basename)) return false;
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Copy src/backend into the project keeping only the chosen backend's files.
|
|
238
|
+
* Shared files (SQL migrations, .htaccess, web.config, README, ...) are kept
|
|
239
|
+
* for both. Empty directories left behind by filtering are not created.
|
|
240
|
+
*/
|
|
241
|
+
function copyBackend(src, dest, backend) {
|
|
242
|
+
const stat = fs.statSync(src);
|
|
243
|
+
if (stat.isDirectory()) {
|
|
244
|
+
for (const child of fs.readdirSync(src)) {
|
|
245
|
+
const childSrc = path.join(src, child);
|
|
246
|
+
const isDir = fs.statSync(childSrc).isDirectory();
|
|
247
|
+
if (!backendEntryKept(child, isDir, backend)) continue;
|
|
248
|
+
copyBackend(childSrc, path.join(dest, child), backend);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
252
|
+
fs.copyFileSync(src, dest);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
188
256
|
function slashComment(content, line) {
|
|
189
257
|
content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
|
|
190
258
|
return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
|
|
@@ -203,7 +271,7 @@ function njkUncomment(content, line) {
|
|
|
203
271
|
return content.split(`{# ${line} #}`).join(line);
|
|
204
272
|
}
|
|
205
273
|
|
|
206
|
-
function installDependencies() {
|
|
274
|
+
function installDependencies(backend) {
|
|
207
275
|
const backendCore = path.join(targetDir, 'src', 'backend', '_core');
|
|
208
276
|
|
|
209
277
|
log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
|
|
@@ -220,6 +288,14 @@ function installDependencies() {
|
|
|
220
288
|
return false;
|
|
221
289
|
}
|
|
222
290
|
|
|
291
|
+
// Node backend: its runtime deps were added to the ROOT package.json and
|
|
292
|
+
// installed above into the root node_modules â there is no separate install
|
|
293
|
+
// inside src/backend, and Composer is never run.
|
|
294
|
+
if (backend === BACKEND.NODE) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- PHP backend: install Composer dependencies (if present) ---
|
|
223
299
|
if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
|
|
224
300
|
return true;
|
|
225
301
|
}
|
|
@@ -353,6 +429,7 @@ async function init() {
|
|
|
353
429
|
|
|
354
430
|
const language = await askChoice('Select a language', LANGUAGE_CHOICES);
|
|
355
431
|
const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
|
|
432
|
+
const backend = await askChoice('Select a backend', BACKEND_CHOICES);
|
|
356
433
|
|
|
357
434
|
log('');
|
|
358
435
|
|
|
@@ -360,11 +437,19 @@ async function init() {
|
|
|
360
437
|
const src = path.join(templateDir, target);
|
|
361
438
|
const dest = path.join(targetDir, target);
|
|
362
439
|
if (!fs.existsSync(src)) continue;
|
|
363
|
-
|
|
364
|
-
|
|
440
|
+
|
|
441
|
+
if (target === 'src/backend') {
|
|
442
|
+
// Copy only the chosen backend's files (the other backend is omitted).
|
|
443
|
+
copyBackend(src, dest, backend);
|
|
444
|
+
} else {
|
|
445
|
+
const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
|
|
446
|
+
copyRecursive(src, dest, exclude);
|
|
447
|
+
}
|
|
365
448
|
logAdd(target);
|
|
366
449
|
}
|
|
367
450
|
|
|
451
|
+
// Generate the local config only for the chosen backend. The other backend's
|
|
452
|
+
// example file was not copied, so its guard below is simply skipped.
|
|
368
453
|
const configDest = path.join(targetDir, 'src/backend/config.php');
|
|
369
454
|
const configExample = path.join(targetDir, 'src/backend/example.config.php');
|
|
370
455
|
if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
|
|
@@ -372,7 +457,24 @@ async function init() {
|
|
|
372
457
|
logAdd('src/backend/config.php');
|
|
373
458
|
}
|
|
374
459
|
|
|
460
|
+
const configJsDest = path.join(targetDir, 'src/backend/config.js');
|
|
461
|
+
const configJsExample = path.join(targetDir, 'src/backend/example.config.js');
|
|
462
|
+
if (!fs.existsSync(configJsDest) && fs.existsSync(configJsExample)) {
|
|
463
|
+
fs.copyFileSync(configJsExample, configJsDest);
|
|
464
|
+
logAdd('src/backend/config.js');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Build the project package.json. Clone the shared dependency maps so we
|
|
468
|
+
// never mutate the PROJECT_PACKAGE constant.
|
|
375
469
|
const pkg = { ...PROJECT_PACKAGE };
|
|
470
|
+
pkg.dependencies = { ...PROJECT_PACKAGE.dependencies };
|
|
471
|
+
pkg.devDependencies = { ...PROJECT_PACKAGE.devDependencies };
|
|
472
|
+
|
|
473
|
+
// Node backend deps live in the ROOT node_modules â add them to root deps
|
|
474
|
+
// so `npm install` installs them there (never in src/backend).
|
|
475
|
+
if (backend === BACKEND.NODE) {
|
|
476
|
+
pkg.dependencies = { ...pkg.dependencies, ...NODE_BACKEND_DEPENDENCIES };
|
|
477
|
+
}
|
|
376
478
|
|
|
377
479
|
if (language === LANGUAGE.TYPESCRIPT) {
|
|
378
480
|
const tsSrc = path.join(templateDir, 'tsconfig.json');
|
|
@@ -395,7 +497,7 @@ async function init() {
|
|
|
395
497
|
applyFramework(framework);
|
|
396
498
|
applyLanguage(language);
|
|
397
499
|
|
|
398
|
-
installDependencies();
|
|
500
|
+
installDependencies(backend);
|
|
399
501
|
|
|
400
502
|
log(`\n${color.green}>> Done!${color.reset}`);
|
|
401
503
|
log(`${color.yellow}\nNow run:\n${color.reset}`);
|
package/docs/Backend.md
CHANGED
|
@@ -1,46 +1,98 @@
|
|
|
1
1
|
# Backend
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
10
39
|
|
|
11
40
|
## Structure
|
|
12
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
|
+
|
|
13
45
|
```
|
|
14
46
|
src/backend/
|
|
15
47
|
âââ api/
|
|
16
|
-
â âââ protected/ # Endpoints requiring X-Api-Key header
|
|
17
48
|
â âââ public/ # Endpoints accessible without an API key
|
|
49
|
+
â âââ protected/ # Endpoints requiring the X-Api-Key header
|
|
18
50
|
âââ _core/ # Framework internals (routing, modules) â do not edit
|
|
19
|
-
â
|
|
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
|
|
20
55
|
âââ database/
|
|
21
|
-
â âââ Database.php
|
|
22
|
-
â
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
64
|
```
|
|
26
65
|
|
|
27
|
-
>
|
|
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.
|
|
28
70
|
|
|
29
|
-
>
|
|
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`.
|
|
30
75
|
|
|
31
|
-
##
|
|
76
|
+
## Key difference: how each backend runs
|
|
32
77
|
|
|
33
|
-
|
|
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`.
|
|
34
83
|
|
|
35
|
-
|
|
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.
|
|
36
89
|
|
|
37
90
|
### config.php <small>(`src/backend/`)</small>
|
|
38
91
|
```php
|
|
39
|
-
// Default key for protected endpoints
|
|
92
|
+
// Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
|
|
40
93
|
'GENERAL_API_KEY' => 'DEFAULT_KEY',
|
|
41
94
|
|
|
42
|
-
//
|
|
43
|
-
// For subfolder endpoints, use the relative path ('subfolder/endpoint')
|
|
95
|
+
// Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
|
|
44
96
|
'CUSTOM_ENDPOINT_KEYS' => [
|
|
45
97
|
'subfolder/endpoint' => 'custom-key',
|
|
46
98
|
],
|
|
@@ -61,39 +113,100 @@ Fill in your values:
|
|
|
61
113
|
'DB_PASS' => '',
|
|
62
114
|
```
|
|
63
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
|
+
|
|
64
135
|
### API keys
|
|
65
136
|
|
|
66
|
-
`GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
|
|
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.
|
|
67
140
|
|
|
68
|
-
> â ïļ The API key travels in the `X-Api-Key` header on every request. Use it only
|
|
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.
|
|
69
144
|
|
|
70
145
|
### CORS (allowed origins)
|
|
71
146
|
|
|
72
147
|
Cross-origin access mirrors the API-key model:
|
|
73
148
|
|
|
74
|
-
- `GENERAL_ALLOWED_ORIGINS` â the default list of origins allowed to call any
|
|
75
|
-
|
|
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`.
|
|
76
153
|
|
|
77
|
-
Origins must be exact (scheme + host, no trailing slash), e.g.
|
|
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).
|
|
78
160
|
|
|
79
|
-
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
|
|
161
|
+
Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
|
|
162
|
+
present, otherwise `GENERAL_ALLOWED_ORIGINS`.
|
|
80
163
|
|
|
81
164
|
## How routing works
|
|
82
165
|
|
|
83
|
-
The file path inside `api/` maps directly to the URL.
|
|
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)
|
|
84
173
|
|
|
85
|
-
|
|
174
|
+
Subfolders **do** appear: `api/public/users/list` â `/api/users/list`. Extra URL
|
|
175
|
+
segments become route parameters. Every endpoint has access to:
|
|
86
176
|
|
|
87
|
-
| Variable | Description |
|
|
177
|
+
| Variable / field | Description |
|
|
88
178
|
|---|---|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
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.
|
|
91
191
|
|
|
92
192
|
## Creating a public endpoint
|
|
93
193
|
|
|
94
|
-
Create a
|
|
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`.
|
|
95
208
|
|
|
96
|
-
|
|
209
|
+
The same endpoint in PHP â identical logic, PHP syntax:
|
|
97
210
|
```php
|
|
98
211
|
<?php
|
|
99
212
|
declare(strict_types=1);
|
|
@@ -111,9 +224,18 @@ Response::success(['id' => $id]);
|
|
|
111
224
|
|
|
112
225
|
## Creating a protected endpoint
|
|
113
226
|
|
|
114
|
-
Create a
|
|
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
|
+
```
|
|
115
237
|
|
|
116
|
-
|
|
238
|
+
The same endpoint in PHP:
|
|
117
239
|
```php
|
|
118
240
|
<?php
|
|
119
241
|
declare(strict_types=1);
|
|
@@ -127,25 +249,48 @@ if ($method !== 'GET') {
|
|
|
127
249
|
Response::success(['visits' => 1024]);
|
|
128
250
|
```
|
|
129
251
|
|
|
130
|
-
To assign a dedicated key, add it to
|
|
252
|
+
To assign a dedicated key, add it to your config:
|
|
131
253
|
|
|
254
|
+
```js
|
|
255
|
+
CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
|
|
256
|
+
```
|
|
132
257
|
```php
|
|
133
|
-
'CUSTOM_ENDPOINT_KEYS' => [
|
|
134
|
-
'endpoint' => 'custom-key',
|
|
135
|
-
],
|
|
258
|
+
'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
|
|
136
259
|
```
|
|
137
260
|
|
|
138
261
|
## The Response helper
|
|
139
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
|
+
```
|
|
140
270
|
```php
|
|
141
271
|
Response::success($data, $code); // default 200
|
|
142
272
|
Response::error($message, $code, $details); // default 400
|
|
143
|
-
Response::noContent();
|
|
273
|
+
Response::noContent(); // 204
|
|
144
274
|
```
|
|
145
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
|
+
|
|
146
291
|
## Pre-built endpoints
|
|
147
292
|
|
|
148
293
|
| Route | Method | Description |
|
|
149
294
|
|---|---|---|
|
|
150
|
-
| `/api/
|
|
151
|
-
| `/api/
|
|
295
|
+
| `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
|
|
296
|
+
| `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |
|