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 +20 -6
- package/_tools/modules/pageComponents.js +1 -1
- package/bin/create.js +39 -3
- package/docs/Backend.md +179 -42
- package/docs/Deploy.md +139 -50
- package/nginx.conf +75 -47
- 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 +51 -16
- package/src/frontend/data/site.json +54 -54
- package/src/frontend/web.config +55 -27
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
|
|
@@ -34,7 +34,7 @@ function addPageBlock(pageName) {
|
|
|
34
34
|
|
|
35
35
|
const block =
|
|
36
36
|
`{% elif title == "${camelName}" %}\n` +
|
|
37
|
-
`{
|
|
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
|
|
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
|
-
|
|
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 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
|
-
â
|
|
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
|
-
âââ
|
|
24
|
-
|
|
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**
|
|
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
|
-
|
|
68
|
+
## Key difference: how each backend runs
|
|
30
69
|
|
|
31
|
-
|
|
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
|
-
|
|
76
|
+
## Configuration
|
|
34
77
|
|
|
35
|
-
|
|
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
|
|
84
|
+
// Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
|
|
40
85
|
'GENERAL_API_KEY' => 'DEFAULT_KEY',
|
|
41
86
|
|
|
42
|
-
//
|
|
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
|
|
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
|
|
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
|
|
75
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
|
90
|
-
|
|
|
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
|
|
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
|
-
|
|
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
|
|
219
|
+
Create a file inside `api/protected/`. The API-key check happens automatically
|
|
220
|
+
before your file runs.
|
|
115
221
|
|
|
116
|
-
### api/protected/example.
|
|
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
|
|
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();
|
|
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/
|
|
151
|
-
| `/api/
|
|
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` |
|