@stonyx/rest-server 0.2.1-beta.8 → 0.2.1-beta.80

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
@@ -1,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/rest-server.svg)](https://www.npmjs.com/package/@stonyx/rest-server)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
1
5
  # @stonyx/rest-server
2
6
 
3
7
  REST server module for the [Stonyx framework](https://github.com/abofs/stonyx), providing dynamic route registration and built-in request handling with optional authentication hooks.
@@ -75,8 +79,21 @@ Configuration is read from `stonyx/config` under `restServer`:
75
79
  | `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
76
80
  | `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
77
81
  | `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
82
+ | `trustProxy` | **Boolean** | `false` | Trust reverse proxy headers (e.g. `X-Forwarded-Proto`). Enable via `REST_TRUST_PROXY=true` when running behind a load balancer such as AWS ALB/ELB to ensure correct protocol detection. |
78
83
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
79
84
 
85
+ ### Running Behind a Load Balancer
86
+
87
+ When your application runs behind a reverse proxy or load balancer (e.g. AWS ALB/ELB), the load balancer terminates SSL and forwards requests to your server over HTTP internally. This means Express sees `http` as the protocol even though the original client request used `https`.
88
+
89
+ To fix this, enable the `trustProxy` option:
90
+
91
+ ```bash
92
+ REST_TRUST_PROXY=true
93
+ ```
94
+
95
+ This tells Express to trust the `X-Forwarded-Proto` header set by the load balancer, so `request.protocol` correctly returns `https`. This is important for any functionality that generates URLs based on the incoming request protocol, such as JSON:API relationship links.
96
+
80
97
  ## Request Class
81
98
 
82
99
  The `Request` class provides a structured way to define route handlers and authorization hooks. Route classes extend `Request` and define:
@@ -3,15 +3,19 @@ const {
3
3
  REST_CORS_METHODS,
4
4
  REST_HEALTH_CHECK_DISABLE,
5
5
  REST_PORT,
6
- REST_REQUEST_PATH
7
- } = process;
6
+ REST_REQUEST_PATH,
7
+ REST_TRUST_PROXY
8
+ } = process.env;
8
9
 
9
- export default {
10
+ const config = {
10
11
  enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
11
12
  origin: REST_CORS_ORIGIN ?? '*',
12
13
  methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
13
14
  dir: REST_REQUEST_PATH ?? './requests',
14
15
  port: REST_PORT ?? 2666,
16
+ trustProxy: REST_TRUST_PROXY === 'true',
15
17
  logColor: 'yellow',
16
18
  logMethod: 'api'
17
19
  };
20
+
21
+ export default config;
package/dist/main.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type Express } from 'express';
2
+ import type { Server } from 'http';
3
+ export { default as Request } from './request.js';
4
+ export default class RestServer {
5
+ static instance: RestServer;
6
+ api: Express;
7
+ server: Server;
8
+ constructor();
9
+ static close(): void;
10
+ init(): Promise<void>;
11
+ setupRouter(): Promise<void>;
12
+ setupGlobalMiddleware(): void;
13
+ mountRoute(routeClassUntyped: unknown, { name, options }: {
14
+ name: string;
15
+ options?: unknown;
16
+ }): void;
17
+ }
18
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAIlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAShB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
package/dist/main.js ADDED
@@ -0,0 +1,86 @@
1
+ /*
2
+ * Copyright 2025 Stone Costa
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the 'License');
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import cors from 'cors';
17
+ import express from 'express';
18
+ import config from 'stonyx/config';
19
+ import log from 'stonyx/log';
20
+ import { forEachFileImport } from '@stonyx/utils/file';
21
+ export { default as Request } from './request.js';
22
+ export default class RestServer {
23
+ static instance;
24
+ api;
25
+ server;
26
+ constructor() {
27
+ if (RestServer.instance)
28
+ return RestServer.instance;
29
+ RestServer.instance = this;
30
+ this.api = express();
31
+ }
32
+ static close() {
33
+ if (!RestServer.instance)
34
+ throw new Error('RestServer has not been initialized yet');
35
+ const { server } = RestServer.instance;
36
+ server.closeAllConnections();
37
+ server.close();
38
+ }
39
+ async init() {
40
+ // Self-register so log.api works even when @stonyx/rest-server is in the
41
+ // consumer's `dependencies` (stonyx loader only merges devDependencies).
42
+ const { logColor = 'yellow', logMethod = 'api' } = config.restServer;
43
+ log.defineType(logMethod, logColor);
44
+ await this.setupRouter();
45
+ const { port } = config.restServer;
46
+ // start REST server
47
+ this.server = this.api.listen(port);
48
+ log.title(`API Server is listening on port ${port}`);
49
+ }
50
+ async setupRouter() {
51
+ const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
52
+ this.setupGlobalMiddleware();
53
+ try {
54
+ await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
55
+ if (enableHealthCheck)
56
+ this.api.get('/health', (_req, res) => res.sendStatus(200));
57
+ }
58
+ catch (error) {
59
+ if (config.debug)
60
+ console.log(error);
61
+ log.error(`Unable to dynamically configure routes from files in ${dir}`);
62
+ throw new Error(`Unable to dynamically configure routes from files in ${dir}`);
63
+ }
64
+ }
65
+ setupGlobalMiddleware() {
66
+ const { origin, methods, trustProxy } = config.restServer;
67
+ if (trustProxy)
68
+ this.api.set('trust proxy', true);
69
+ this.api.use([
70
+ cors({ origin, methods }),
71
+ express.json()
72
+ ]);
73
+ }
74
+ mountRoute(routeClassUntyped, { name, options }) {
75
+ const routeClass = routeClassUntyped;
76
+ const { api } = this;
77
+ const classInstance = new routeClass(options);
78
+ const route = name === 'index' ? '/' : `/${name}`;
79
+ const { expressInstance } = classInstance;
80
+ classInstance.registerCalls();
81
+ expressInstance.mountpath = route;
82
+ // Mount handler to main api instance
83
+ api.use(route, expressInstance);
84
+ }
85
+ }
86
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type Request as ExpressRequest, type Response as ExpressResponse, type Express } from 'express';
2
+ export type RequestState = Record<string, unknown>;
3
+ export type RequestHandler = (req: ExpressRequest, state: RequestState) => unknown | Promise<unknown>;
4
+ export type AuthHandler = (req: ExpressRequest, state: RequestState) => number | undefined;
5
+ export type RouteHandlers = Record<string, Record<string, RequestHandler | RequestHandler[]>>;
6
+ export default class Request {
7
+ static stateProp: string;
8
+ static getState(req: ExpressRequest): RequestState;
9
+ static sendStatusResponse(res: ExpressResponse, status: number): void;
10
+ expressInstance: Express;
11
+ handlers: RouteHandlers;
12
+ auth?: AuthHandler;
13
+ constructor();
14
+ registerCalls(): void;
15
+ }
16
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAMlH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IAS3B,aAAa,IAAI,IAAI;CAqDtB"}
@@ -0,0 +1,87 @@
1
+ import express from 'express';
2
+ import config from 'stonyx/config';
3
+ import { makeArray } from '@stonyx/utils/object';
4
+ const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
5
+ export default class Request {
6
+ static stateProp = '__stonyxState';
7
+ static getState(req) {
8
+ const { stateProp } = Request;
9
+ const record = req;
10
+ if (record[stateProp] !== undefined)
11
+ return record[stateProp];
12
+ record[stateProp] = {};
13
+ return record[stateProp];
14
+ }
15
+ static sendStatusResponse(res, status) {
16
+ const statusMap = config.restServer?.statusMap ?? {};
17
+ const message = statusMap[status] || '';
18
+ if (message) {
19
+ res.status(status).send(message);
20
+ }
21
+ else {
22
+ res.sendStatus(status);
23
+ }
24
+ }
25
+ expressInstance;
26
+ handlers;
27
+ constructor() {
28
+ const api = express();
29
+ api.disable('x-powered-by');
30
+ this.expressInstance = api;
31
+ }
32
+ registerCalls() {
33
+ const { expressInstance } = this;
34
+ const { getState, sendStatusResponse } = Request;
35
+ for (const [method, handlers] of Object.entries(this.handlers)) {
36
+ if (!METHODS.has(method)) {
37
+ console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
38
+ continue;
39
+ }
40
+ for (const [route, handler] of Object.entries(handlers)) {
41
+ expressInstance[method](route, async (req, res) => {
42
+ // Run auth after route matching so request.params is populated
43
+ if (this.auth) {
44
+ const status = this.auth(req, getState(req));
45
+ if (status)
46
+ return sendStatusResponse(res, status);
47
+ }
48
+ const callStack = [...makeArray(handler)];
49
+ const mainCall = callStack.pop();
50
+ let response;
51
+ // Run middleware
52
+ while (callStack.length) {
53
+ response = await callStack.shift().bind(this)(req, getState(req));
54
+ if (response !== undefined)
55
+ break;
56
+ }
57
+ if (response === undefined)
58
+ response = await mainCall(req, getState(req));
59
+ if (Number.isInteger(response))
60
+ return sendStatusResponse(res, response);
61
+ // Handle redirect if set via call state object
62
+ const state = getState(req);
63
+ const { redirect } = state;
64
+ if (redirect)
65
+ return res.redirect(redirect);
66
+ // Handle pipe if set via call state object
67
+ const { pipe } = state;
68
+ if (pipe) {
69
+ const { headers, source } = pipe;
70
+ if (headers)
71
+ for (const [key, value] of Object.entries(headers))
72
+ res.set(key, value);
73
+ return source.pipe(res);
74
+ }
75
+ if (response === undefined) {
76
+ res.sendStatus(200);
77
+ return;
78
+ }
79
+ if (typeof response !== 'object')
80
+ return sendStatusResponse(res, 500);
81
+ res.send(response);
82
+ });
83
+ }
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -4,22 +4,31 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.8",
7
+ "version": "0.2.1-beta.80",
8
8
  "description": "Rest Server Module for Stonyx Framework",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "https://github.com/abofs/stonyx-rest-server"
12
12
  },
13
- "main": "src/main.js",
13
+ "main": "dist/main.js",
14
+ "types": "dist/main.d.ts",
14
15
  "type": "module",
15
16
  "exports": {
16
- ".": "./src/main.js"
17
+ ".": {
18
+ "types": "./dist/main.d.ts",
19
+ "default": "./dist/main.js"
20
+ }
17
21
  },
18
22
  "author": "Stone Costa",
19
23
  "license": "Apache-2.0",
20
24
  "contributors": [
21
25
  "Stone Costa <stone.costa@synamicd.com>"
22
26
  ],
27
+ "files": [
28
+ "dist",
29
+ "config",
30
+ "README.md"
31
+ ],
23
32
  "publishConfig": {
24
33
  "access": "public",
25
34
  "provenance": true
@@ -27,14 +36,23 @@
27
36
  "dependencies": {
28
37
  "cors": "^2.8.5",
29
38
  "express": "^5.1.0",
30
- "stonyx": "0.2.3-beta.3"
39
+ "stonyx": "0.2.3-beta.76"
31
40
  },
32
41
  "devDependencies": {
33
- "@stonyx/utils": "0.2.3-beta.4",
42
+ "@stonyx/utils": "0.2.3-beta.26",
43
+ "@types/cors": "^2.8.17",
44
+ "@types/express": "^5.0.6",
45
+ "@types/node": "^25.5.2",
46
+ "@types/qunit": "^2.19.13",
47
+ "@types/sinon": "^21.0.1",
34
48
  "qunit": "^2.24.1",
35
- "sinon": "^21.0.0"
49
+ "sinon": "^21.0.0",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^5.8.3"
36
52
  },
37
53
  "scripts": {
38
- "test": "stonyx test"
54
+ "build": "tsc",
55
+ "typecheck": "tsc -p tsconfig.test.json",
56
+ "test": "pnpm build && pnpm typecheck && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
39
57
  }
40
58
  }
@@ -1,44 +0,0 @@
1
- # Improvement Opportunities
2
-
3
- ## 1. `new express()` in RestServer constructor
4
-
5
- **File**: `src/main.js`, line 30
6
-
7
- ```js
8
- this.api = new express();
9
- ```
10
-
11
- Express 5 (which this project targets via `"express": "^5.1.0"`) documents calling `express()` as a plain function, not as a constructor with `new`. While `new express()` works in practice because the function returns a new object regardless, it is unconventional and may break if Express ever enforces non-constructor semantics. Consider changing to:
12
-
13
- ```js
14
- this.api = express();
15
- ```
16
-
17
- Note: The `Request` class in `src/request.js` line 26 already uses the correct pattern (`const api = express();` without `new`).
18
-
19
- ## 2. `setupGlobalMiddleware` is `async` but contains no awaits
20
-
21
- **File**: `src/main.js`, lines 63-70
22
-
23
- ```js
24
- async setupGlobalMiddleware() {
25
- const { origin, methods } = config.restServer;
26
-
27
- this.api.use([
28
- cors({ origin, methods }),
29
- express.json()
30
- ]);
31
- }
32
- ```
33
-
34
- The `async` keyword is unnecessary here since the method body contains no `await` expressions and `app.use()` is synchronous. The `async` keyword causes the method to return a `Promise` wrapping `undefined`, but the caller (`setupRouter`) does not `await` it either — it calls `this.setupGlobalMiddleware()` without `await` on line 52. Removing `async` would make the intent clearer.
35
-
36
- ## 3. `logMethod` config option is not used in rest-server source
37
-
38
- **File**: `config/environment.js`, line 16
39
-
40
- ```js
41
- logMethod: 'api'
42
- ```
43
-
44
- The `logMethod` property is defined in the default config but is never referenced anywhere in `src/main.js` or `src/request.js`. It is likely consumed by the Stonyx framework core (`stonyx/log`) for registering a named log method, but this should be verified. If it is indeed framework-level plumbing, consider documenting that it is a Stonyx convention rather than a rest-server feature. If it is unused, consider removing it to reduce config surface area.
@@ -1,149 +0,0 @@
1
- # @stonyx/rest-server — Project Structure
2
-
3
- ## Overview
4
-
5
- REST server module for the Stonyx framework. Provides dynamic route registration from a file directory, built-in CORS/JSON middleware, per-route authorization hooks, and a structured `Request` base class for defining handlers.
6
-
7
- - **Package**: `@stonyx/rest-server` (v0.2.1-beta.1)
8
- - **License**: Apache-2.0
9
- - **Entry point**: `src/main.js`
10
- - **Module type**: ESM (`"type": "module"`)
11
- - **Node version**: v24.13.0 (per `.nvmrc`)
12
- - **Package manager**: pnpm
13
-
14
- ## Architecture
15
-
16
- ### RestServer (src/main.js)
17
-
18
- Singleton class wrapping an Express 5 instance.
19
-
20
- - **Constructor** — enforces singleton via `RestServer.instance`; creates the Express app with `new express()`
21
- - **`init()`** — calls `setupRouter()`, then starts listening on the configured port
22
- - **`setupRouter()`** — calls `setupGlobalMiddleware()`, then uses `forEachFileImport` (from `@stonyx/utils/file`) to dynamically import all files in the configured `dir` and mount each as a route via `mountRoute()`. Optionally registers a `/health` endpoint.
23
- - **`setupGlobalMiddleware()`** — attaches `cors()` and `express.json()` middleware to the Express app
24
- - **`mountRoute(routeClass, { name, options })`** — instantiates the imported Request subclass, wires up the `authorization` middleware if present, calls `registerCalls()`, and mounts the sub-app at `/<filename>`
25
- - **`RestServer.close()`** — static method to close the server
26
-
27
- ### Request (src/request.js)
28
-
29
- Base class for route definitions. Each file in the requests directory exports a class extending `Request`.
30
-
31
- - **Constructor** — creates a child Express instance with `x-powered-by` disabled
32
- - **`handlers`** — instance property: object mapping HTTP methods (`get`, `post`, `put`, `delete`, `patch`) to route-path/handler pairs
33
- - **`auth(req, state)`** — optional hook. Return an integer status code to reject the request; return nothing to allow it through.
34
- - **`authorization(req, res, next)`** — wrapper that calls `auth()` and short-circuits with a status response if it returns a code
35
- - **`registerCalls()`** — iterates `handlers`, registers each route on the child Express instance. Supports:
36
- - Single handler function or array (last element is the main handler, preceding elements are middleware)
37
- - Middleware functions are bound to the class instance and executed in order
38
- - Integer return = status code response (via `sendStatusResponse`)
39
- - Object return = JSON response
40
- - `undefined` return = 200 OK
41
- - Pipe support via `state.pipe` (sets headers and pipes a stream)
42
- - **`Request.getState(req)`** — attaches/retrieves a `__stonyxState` object on the Express request
43
- - **`Request.sendStatusResponse(res, status)`** — sends status with optional custom message from `config.restServer.statusMap`
44
-
45
- Valid HTTP methods (enforced): `get`, `post`, `put`, `delete`, `patch`
46
-
47
- ## Configuration Reference
48
-
49
- From `config/environment.js`. All values are overridable via environment variables.
50
-
51
- | Option | Type | Default | Env Var | Description |
52
- |---------------------|-------------------|-------------------------------|----------------------------|-----------------------------------------------------------------|
53
- | `enableHealthCheck` | **Boolean** | `true` | `REST_HEALTH_CHECK_DISABLE=true` to disable | Registers `GET /health` returning 200 |
54
- | `origin` | **String** | `'*'` | `REST_CORS_ORIGIN` | CORS allowed origin(s) |
55
- | `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | `REST_CORS_METHODS` | CORS allowed methods |
56
- | `dir` | **String** | `'./requests'` | `REST_REQUEST_PATH` | Directory containing Request class files to mount as routes |
57
- | `port` | **Number/String** | `2666` | `REST_PORT` | Port the REST server listens on |
58
- | `logColor` | **String** | `'yellow'` | — | Console log color for this module (Stonyx logging integration) |
59
- | `logMethod` | **String** | `'api'` | — | Log method name (Stonyx logging integration) |
60
-
61
- Additional config used (not rest-server-specific):
62
- - `config.debug` (top-level Stonyx config) — if truthy, logs errors during route setup
63
- - `config.restServer.statusMap` (optional, no default in environment.js) — maps status codes to custom message strings
64
- - `config.restServer.camelCaseRoutes` (optional, no default in environment.js) — when falsy, passes `rawName: true` to `forEachFileImport` so filenames are used as-is for route paths
65
-
66
- ## Test Structure
67
-
68
- Tests use **QUnit** and run via `stonyx test` (the `npm test` script).
69
-
70
- ### test/config/environment.js
71
- Overrides `restServer.dir` to `'./test/sample/requests'` so tests load sample request classes.
72
-
73
- ### test/unit/request-test.js
74
- Unit tests for `Request` static methods:
75
- - `getState` — creates/returns state object on request
76
- - `sendStatusResponse` — sends status with optional `statusMap` message
77
-
78
- ### test/integration/rest-server-test.js
79
- Integration tests that boot the full server and make HTTP requests:
80
- - 404 for non-existent routes
81
- - `/public` — JSON response, 200 OK default, URL params, middleware (success/failure), `this` binding for handlers and middleware
82
- - `/private` — authenticated success, auth hook rejection (505)
83
- - `/health` — health check endpoint returns 200
84
-
85
- ### test/sample/requests/
86
- Sample Request subclasses used by integration tests:
87
- - `public.js` — `PublicRequest` with various GET handlers demonstrating middleware, params, binding
88
- - `private.js` — `PrivateRequest` with `auth()` hook that rejects `/failure` with 505
89
-
90
- ## CI/CD
91
-
92
- ### .github/workflows/ci.yml
93
- Runs on pull requests to `dev` and `main`. Delegates to shared workflow at `abofs/stonyx-workflows`.
94
-
95
- ### .github/workflows/publish.yml
96
- Publishes to NPM. Triggered by:
97
- - `workflow_dispatch` with version-type selection (patch/minor/major) or custom version
98
- - Pull requests to `main`/`dev`
99
- - Pushes to `main`
100
-
101
- Delegates to `abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main`.
102
-
103
- ## File Structure
104
-
105
- ```
106
- stonyx-rest-server/
107
- ├── .claude/
108
- │ ├── improvements.md
109
- │ └── project-structure.md # this file
110
- ├── .github/
111
- │ └── workflows/
112
- │ ├── ci.yml # PR CI — delegates to shared workflow
113
- │ └── publish.yml # NPM publish — delegates to shared workflow
114
- ├── config/
115
- │ └── environment.js # Default config with env var overrides
116
- ├── src/
117
- │ ├── main.js # RestServer class (singleton, Express wrapper)
118
- │ └── request.js # Request base class (handler registration, auth hook)
119
- ├── test/
120
- │ ├── config/
121
- │ │ └── environment.js # Test config override (dir → test/sample/requests)
122
- │ ├── integration/
123
- │ │ └── rest-server-test.js # Integration tests (QUnit)
124
- │ ├── sample/
125
- │ │ └── requests/
126
- │ │ ├── private.js # Sample private request with auth hook
127
- │ │ └── public.js # Sample public request with middleware demos
128
- │ └── unit/
129
- │ └── request-test.js # Unit tests for Request statics (QUnit)
130
- ├── .gitignore
131
- ├── .npmignore
132
- ├── .nvmrc # Node v24.13.0
133
- ├── LICENSE.md # Apache 2.0
134
- ├── package.json
135
- ├── pnpm-lock.yaml
136
- └── README.md
137
- ```
138
-
139
- ## Dependencies
140
-
141
- ### Runtime
142
- - `cors` ^2.8.5 — CORS middleware
143
- - `express` ^5.1.0 — HTTP framework
144
- - `stonyx` (local link) — Framework core (config, logging)
145
-
146
- ### Dev
147
- - `@stonyx/utils` (local link) — Utility functions (file import, object helpers)
148
- - `qunit` ^2.24.1 — Test framework
149
- - `sinon` ^21.0.0 — Test stubs/spies
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [dev, main]
6
-
7
- concurrency:
8
- group: ci-${{ github.head_ref || github.ref }}
9
- cancel-in-progress: true
10
-
11
- permissions:
12
- contents: read
13
-
14
- jobs:
15
- test:
16
- uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
@@ -1,51 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- repository_dispatch:
5
- types: [cascade-publish]
6
- workflow_dispatch:
7
- inputs:
8
- version-type:
9
- description: 'Version type'
10
- required: true
11
- type: choice
12
- options:
13
- - patch
14
- - minor
15
- - major
16
- custom-version:
17
- description: 'Custom version (optional, overrides version-type)'
18
- required: false
19
- type: string
20
- pull_request:
21
- types: [opened, synchronize, reopened]
22
- branches: [main]
23
- push:
24
- branches: [main]
25
-
26
- concurrency:
27
- group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
28
- cancel-in-progress: false
29
-
30
- permissions:
31
- contents: write
32
- id-token: write
33
- pull-requests: write
34
-
35
- jobs:
36
- publish:
37
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
38
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
39
- with:
40
- version-type: ${{ github.event.inputs.version-type }}
41
- custom-version: ${{ github.event.inputs.custom-version }}
42
- cascade-source: ${{ github.event.client_payload.source_package || '' }}
43
- secrets: inherit
44
-
45
- cascade:
46
- needs: publish
47
- uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
48
- with:
49
- package-name: ${{ needs.publish.outputs.package-name }}
50
- published-version: ${{ needs.publish.outputs.published-version }}
51
- secrets: inherit
package/src/main.js DELETED
@@ -1,89 +0,0 @@
1
- /*
2
- * Copyright 2025 Stone Costa
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the 'License');
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- import cors from 'cors';
18
- import express from 'express';
19
- import config from 'stonyx/config';
20
- import log from 'stonyx/log';
21
- import { forEachFileImport } from '@stonyx/utils/file';
22
-
23
- export { default as Request } from './request.js';
24
-
25
- export default class RestServer {
26
- constructor() {
27
- if (RestServer.instance) return RestServer.instance;
28
- RestServer.instance = this;
29
-
30
- this.api = new express();
31
- }
32
-
33
- static close() {
34
- if (!RestServer.instance) throw new Error('RestServer has not been initialized yet');
35
-
36
- RestServer.instance.server.close();
37
- }
38
-
39
- async init() {
40
- await this.setupRouter();
41
-
42
- const { port } = config.restServer;
43
-
44
- // start REST server
45
- this.server = this.api.listen(port);
46
- log.title(`API Server is listening on port ${port}`);
47
- }
48
-
49
- async setupRouter() {
50
- const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
51
- this.setupGlobalMiddleware();
52
-
53
- try {
54
- await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
55
-
56
- if (enableHealthCheck) this.api.get('/health', (_req, res) => res.sendStatus(200));
57
- } catch (error) {
58
- if (config.debug) console.log(error);
59
- throw log.error(`Unable to dynamically configure routes from files in ${dir}`);
60
- }
61
- }
62
-
63
- async setupGlobalMiddleware() {
64
- const { origin, methods } = config.restServer;
65
-
66
- this.api.use([
67
- cors({ origin, methods }),
68
- express.json()
69
- ]);
70
- }
71
-
72
- async mountRoute(routeClass, { name, options }) {
73
- const { api } = this;
74
- const classInstance = new routeClass(options);
75
- const route = name === 'index' ? '/' : `/${name}`;
76
- const { expressInstance, authorization } = classInstance;
77
-
78
- const routeCalls = [ expressInstance ];
79
-
80
- // Assign auth callback if it exists
81
- if (authorization) routeCalls.unshift(authorization.bind(classInstance));
82
-
83
- classInstance.registerCalls();
84
- expressInstance.mountpath = route;
85
-
86
- // Mount handler to main api instance
87
- api.use(route, ...routeCalls);
88
- }
89
- }
package/src/request.js DELETED
@@ -1,85 +0,0 @@
1
- import express from 'express';
2
- import config from 'stonyx/config';
3
- import { makeArray } from '@stonyx/utils/object';
4
-
5
- const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
6
-
7
- export default class Request {
8
- static stateProp = '__stonyxState';
9
-
10
- static getState(req) {
11
- const { stateProp } = Request;
12
- if (req[stateProp] !== undefined) return req[stateProp];
13
-
14
- req[stateProp] = {};
15
- return req[stateProp];
16
- }
17
-
18
- static sendStatusResponse(res, status) {
19
- const statusMap = config.restServer?.statusMap || {};
20
- const message = statusMap[status] || '';
21
-
22
- return message ? res.status(status).send(message) : res.sendStatus(status);
23
- }
24
-
25
- constructor() {
26
- const api = express();
27
- api.disable('x-powered-by');
28
-
29
- this.expressInstance = api;
30
- }
31
-
32
- // auth hook wrapper
33
- authorization(req, res, next) {
34
- if (!this.auth) return next();
35
-
36
- const status = this.auth(req, Request.getState(req));
37
- if (status) return Request.sendStatusResponse(res, status);
38
-
39
- next();
40
- }
41
-
42
- registerCalls() {
43
- const { expressInstance } = this;
44
- const { getState } = Request;
45
-
46
- for (const [method, handlers] of Object.entries(this.handlers)) {
47
- if (!METHODS.has(method)) {
48
- console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
49
- continue;
50
- }
51
-
52
- for (const [route, handler] of Object.entries(handlers)) {
53
- expressInstance[method](route, async (req, res) => {
54
- const callStack = [...makeArray(handler)];
55
- const mainCall = callStack.pop();
56
- const { sendStatusResponse } = Request;
57
- let response;
58
-
59
- // Run middleware
60
- while(callStack.length) {
61
- response = await callStack.shift().bind(this)(req, getState(req));
62
- if (response !== undefined) break;
63
- }
64
-
65
- if (response === undefined) response = await mainCall(req, getState(req));
66
- if (Number.isInteger(response)) return sendStatusResponse(res, response);
67
-
68
- // Handle pipe if set via call state object
69
- const { pipe } = getState(req);
70
- if (pipe) {
71
- const { headers, source } = pipe;
72
-
73
- if (headers) for (const [key, value] of Object.entries(headers)) res.set(key, value);
74
- return source.pipe(res);
75
- }
76
-
77
- if (response === undefined) return res.sendStatus(200);
78
- if (typeof response !== 'object') return sendStatusResponse(res, 500);
79
-
80
- res.send(response);
81
- });
82
- }
83
- }
84
- }
85
- }