maxserver 0.8.2 → 0.8.4

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/.github/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # maxserver
2
2
  Node server setup based on **Fastify** to speed up backend development.
3
- maxserver stands for **maximized simplicity** and **minimum boilerplate**.
4
3
 
5
4
  - **Auto Routes**: auto imports and registers routes and schemas
6
5
  - **Auto Docs**: auto generates docs based on schemas
@@ -123,9 +122,6 @@ export default {
123
122
  };
124
123
  ```
125
124
 
126
- **‼️ Important use export default**
127
- Some examples in the template folder.
128
-
129
125
 
130
126
  ### MODELS
131
127
  You can also auto register **models** (schemas which are shared between multiple routes).
@@ -134,6 +130,9 @@ schema or generic model, by looking if a sibling file exist or not 😉
134
130
 
135
131
  <br>
136
132
 
133
+ **‼️ Important use export default**
134
+ Some examples in the template folder.
135
+
137
136
 
138
137
 
139
138
  ## 📚 API Docs
@@ -145,6 +144,20 @@ And you can also easily test any route.
145
144
 
146
145
 
147
146
 
147
+ ## Global Named Exports
148
+
149
+ Every named export across your JavaScript files is automatically assigned to the Node.js `global` object on startup. This makes your utility functions, constants, or services instantly accessible anywhere in the application without manual `import` statements. The system safely ignores `default` exports and lifecycle hooks, and it will immediately halt with a clear console error if it detects duplicate variable names across different files.
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
148
161
 
149
162
 
150
163
  ## 🔐 Authentication
@@ -217,10 +230,22 @@ Rule of thumb: make the message something you would want to see at 03:00 in logs
217
230
  <br>
218
231
 
219
232
 
233
+ ## Autoregister Hooks
234
+
235
+ Exported functions starting with `autoregister_` automatically execute on startup and receive the Fastify `app` instance. This allows files to self-inject custom hooks, plugins, or configurations locally.
236
+
237
+ ### Example
238
+ ```javascript
239
+ // In any standard .js file
240
+ export async function autoregister_custom_auth(app) {
241
+ app.addHook("onRequest", async (req, reply) => {
242
+ // Local hook logic here
243
+ });
244
+ }
245
+ ```
246
+
247
+
220
248
 
221
- ## Note
222
- On loading routes - possible side effects execute.
223
- Means you can eg declare globals.
224
249
 
225
250
  ## About
226
251
  - Dependencies: original fastify packages + scalar/fastify-api-reference
package/index.d.ts CHANGED
@@ -8,16 +8,21 @@ declare global {
8
8
 
9
9
  var global: typeof globalThis;
10
10
 
11
- /** Casts a string ID to a MongoDB ObjectId using the global helper [cite: 2026-02-15]. */
11
+ var ENV: NodeJS.ProcessEnv & {
12
+ development: boolean;
13
+ production: boolean;
14
+ };
15
+
16
+ /** Casts a string ID to a MongoDB ObjectId using the global helper. */
12
17
  var oid: (id?: string) => import("mongodb").ObjectId;
13
18
 
14
- /** Access to the global MongoDB database instance [cite: 2026-02-15]. */
19
+ /** Access to the global MongoDB database instance. */
15
20
  var db: import("mongodb").Db;
16
21
 
17
22
  /**
18
- * Creates an Error object with an attached HTTP status code [cite: 2026-02-15].
23
+ * Creates an Error object with an attached HTTP status code.
19
24
  * Fastify catches this to return a structured JSON response.
20
- * * @param code The HTTP status code (e.g., 400, 401, 403, 404).
25
+ * @param code The HTTP status code (e.g., 400, 401, 403, 404).
21
26
  * @param message The specific failure reason returned in the JSON body.
22
27
  */
23
28
  var createError: (code: number, message: string) => Error;
@@ -31,6 +36,7 @@ declare module "fastify" {
31
36
  secret?: string;
32
37
  mongodb?: string;
33
38
  static?: string;
39
+ errorLogger?: boolean;
34
40
  };
35
41
  }
36
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxserver",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Node server setup based fastify",
5
5
  "author": "Max Matinpalo",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -7,56 +7,54 @@ import {
7
7
  setupMongo,
8
8
  setupStatic,
9
9
  setupCookie,
10
+ setupErrorLogger,
10
11
  } from "./setup.js";
11
12
 
12
-
13
13
  import { getAddress } from "./getAddress.js";
14
14
  import { setupDocs } from "./setupDocs.js";
15
15
  import { setupRoutes } from "./setupRoutes.js";
16
16
  import { setupDevSounds } from "./devSounds.js";
17
17
 
18
-
19
18
  import fastifyWebsocket from "@fastify/websocket";
20
19
 
21
20
  export default async function maxserver(config = {}) {
22
-
23
-
24
21
  const {
25
-
26
- // maxserver options
27
22
  port = Number(process.env.PORT || 3000),
28
23
  secret = process.env.SECRET,
29
24
  mongodb = process.env.MONGODB,
30
25
  docs = process.env.DOCS !== "false",
31
26
  cors = process.env.CORS || "*",
32
- env = process.env.NODE_ENV || "development", // should be removed, define via env only
27
+ env = process.env.NODE_ENV || "development",
33
28
  routesDir = process.env.ROUTESDIR || "src",
34
29
  scalar = {},
35
30
  openapiInfo,
36
31
  sounds,
37
32
  static: isStatic = process.env.STATIC,
38
33
  public: isPublic = process.env.PUBLIC === "true",
34
+ errorLogger = process.env.ERROR_LOGGER === "true",
39
35
 
40
- // everything else goes straight to Fastify
41
36
  ...fastifyOpts
42
-
43
37
  } = config;
44
38
 
39
+ globalThis.ENV = {
40
+ ...process.env,
41
+ development: process.env.NODE_ENV !== "production",
42
+ production: process.env.NODE_ENV === "production"
43
+ };
44
+
45
45
  const maxserverConfig = {
46
46
  port, secret, mongodb, docs, cors, env, openapiInfo, routesDir, scalar, sounds,
47
47
  static: isStatic,
48
- public: isPublic
48
+ public: isPublic,
49
+ errorLogger
49
50
  };
50
51
 
51
- if (!secret)
52
- throw new Error("secret is must have");
53
-
52
+ if (!secret) throw new Error("secret is must have");
54
53
 
55
54
  let app;
56
55
  try {
57
56
  app = Fastify({
58
57
  trustProxy: true,
59
- // Required to allow adding doc fields on schema
60
58
  ajv: { customOptions: { strictSchema: false } },
61
59
  ...fastifyOpts
62
60
  });
@@ -65,19 +63,19 @@ export default async function maxserver(config = {}) {
65
63
  throw err;
66
64
  }
67
65
 
68
-
69
66
  app.decorate("maxserver", maxserverConfig);
70
67
 
71
68
  app.decorate("start", async function () {
72
69
  const port = this.maxserver.port ?? 3000;
73
- const host = this.maxserver.public ? '0.0.0.0' : '127.0.0.1';
70
+ const host = this.maxserver.public ? "0.0.0.0" : "127.0.0.1";
74
71
  await this.listen({ port, host });
75
- console.log('🟢 ', getAddress(this));
72
+ console.log("🟢 ", getAddress(this));
76
73
  });
77
74
 
78
75
  app.register(fastifyWebsocket);
79
76
 
80
77
  await setupDevSounds(app);
78
+ await setupErrorLogger(app);
81
79
  await setupCookie(app);
82
80
  await setupHelmet(app);
83
81
  await setupCors(app);
@@ -87,7 +85,6 @@ export default async function maxserver(config = {}) {
87
85
  await setupDocs(app);
88
86
  await setupRoutes(app);
89
87
 
90
-
91
88
  global.createError = function (code, message) {
92
89
  const err = new Error(message);
93
90
  err.statusCode = code;
package/src/setup.js CHANGED
@@ -8,8 +8,6 @@ import mongodb from "@fastify/mongodb";
8
8
  import fastifyStatic from "@fastify/static";
9
9
  import helmet from "@fastify/helmet";
10
10
 
11
-
12
-
13
11
  export async function setupHelmet(app) {
14
12
  await app.register(helmet, {
15
13
  contentSecurityPolicy: false,
@@ -20,16 +18,12 @@ export async function setupHelmet(app) {
20
18
  });
21
19
  }
22
20
 
23
-
24
-
25
21
  export async function setupCors(app) {
26
22
  const isProd = app.maxserver.env === "production";
27
23
  let origin = app.maxserver.cors ?? "*";
28
24
 
29
- // Fix: Credentials + "*" = Browser Error
30
- // If no origin is defined in dev, we should allow the specific requester
31
25
  if (origin === "*" && !isProd)
32
- origin = true; // Fastify-cors treats 'true' as "reflect the request origin"
26
+ origin = true;
33
27
 
34
28
  if (isProd && (origin === "*" || origin === true))
35
29
  app.log.warn("CORS: allowing all origins in production with credentials is risky");
@@ -37,12 +31,10 @@ export async function setupCors(app) {
37
31
  await app.register(cors, {
38
32
  origin,
39
33
  credentials: true,
40
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
34
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
41
35
  });
42
36
  }
43
37
 
44
-
45
-
46
38
  export async function setupCookie(app) {
47
39
  await app.register(cookie, {
48
40
  secret: app.maxserver.secret,
@@ -50,9 +42,6 @@ export async function setupCookie(app) {
50
42
  });
51
43
  }
52
44
 
53
-
54
-
55
-
56
45
  export async function setupMongo(app) {
57
46
  const url = app.maxserver.mongodb;
58
47
  if (!url) return;
@@ -63,8 +52,6 @@ export async function setupMongo(app) {
63
52
  global.db = db;
64
53
  }
65
54
 
66
-
67
-
68
55
  export async function setupJwt(app) {
69
56
  await app.register(jwt, {
70
57
  secret: app.maxserver.secret,
@@ -72,25 +59,17 @@ export async function setupJwt(app) {
72
59
  });
73
60
 
74
61
  app.addHook("preHandler", async function (req) {
75
-
76
- // Let preflight requests pass
77
62
  if (req.method === "OPTIONS") return;
78
63
 
79
64
  const auth = req.routeOptions?.config?.auth;
80
-
81
65
  if (!auth) return;
82
66
 
83
67
  await req.jwtVerify();
84
68
  const u = req.user;
85
69
  req.userId = u?.sub || u?.userId || u?.userid || u?.id || null;
86
70
  });
87
-
88
71
  }
89
72
 
90
-
91
-
92
-
93
-
94
73
  export async function setupStatic(app) {
95
74
  const dir = app.maxserver.static;
96
75
  if (!dir) return;
@@ -109,5 +88,17 @@ export async function setupStatic(app) {
109
88
  await app.register(fastifyStatic, { root: abs });
110
89
  }
111
90
 
91
+ export async function setupErrorLogger(app) {
92
+ if (!app.maxserver.errorLogger) return;
112
93
 
94
+ app.addHook("onError", async (req, res, error) => {
95
+ console.log("\n‼️ ERROR ‼️");
113
96
 
97
+ const stackLine = error.stack.split("\n")[1];
98
+ const match = stackLine.match(/([^\s()]+):(\d+):\d+/);
99
+ const file = match?.[1].replace("file://" + process.cwd(), "");
100
+
101
+ if (file) console.log(`${file} -> line ${match[2]}`);
102
+ console.log(error.message);
103
+ });
104
+ }
package/src/setupDocs.js CHANGED
@@ -2,37 +2,6 @@ import swagger from "@fastify/swagger";
2
2
  import apiReference from "@scalar/fastify-api-reference";
3
3
 
4
4
 
5
- /*
6
- const schema = {
7
- summary: "OpenAPI Specification",
8
- description: "Returns the full OpenAPI 3.0 specification.",
9
- tags: ["Docs"],
10
- response: {
11
- 200: {
12
- type: "object",
13
- additionalProperties: true,
14
- required: ["openapi", "info", "paths"],
15
- properties: {
16
- openapi: { type: "string", example: "3.0.3" },
17
- info: {
18
- type: "object",
19
- required: ["title", "version"],
20
- properties: {
21
- title: { type: "string", example: "API" },
22
- version: { type: "string", example: "1.0.0" }
23
- }
24
- },
25
- paths: { type: "object", example: {} }
26
- }
27
- }
28
- }
29
- };
30
- */
31
-
32
-
33
-
34
-
35
-
36
5
  export async function setupDocs(app) {
37
6
 
38
7
  const info = app.maxserver.openapiInfo || {
@@ -76,7 +45,7 @@ export async function setupDocs(app) {
76
45
  persistAuth: true,
77
46
  showDeveloperTools: "never",
78
47
  //"expandAllModelSections": true,
79
- operationsSorter: "alpha",
48
+ // operationsSorter: "alpha",
80
49
  orderSchemaPropertiesBy: "preserve",
81
50
  metaData: {
82
51
  title: "API Docs 👨‍💻",
@@ -54,64 +54,77 @@ function getRoute(file) {
54
54
  };
55
55
  }
56
56
 
57
- /**
58
- * Safe dynamic import for ESM.
59
- */
60
- async function importDefault(file) {
61
- return (await import(pathToFileURL(file).href)).default;
62
- }
63
-
64
57
  export async function setupRoutes(app) {
65
58
  const root = path.resolve(app.maxserver.routesDir || "src");
66
59
  const files = walk(root);
67
60
 
68
- // 1. Pass One: Register Global Schemas (Lonely .schema.js files)
61
+ // 1. Pass One: Register Named Exports Globally
62
+ const globals = new Map();
69
63
  for (const file of files) {
70
- // Only process if no matching handler file exists
71
- if (file.endsWith(".schema.js") && !fs.existsSync(file.replace(".schema.js", ".js"))) {
72
- const mod = await import(pathToFileURL(file).href);
64
+ if (file.endsWith(".schema.js")) continue;
73
65
 
74
- // Register default export + all named exports if they have an $id
75
- for (const schema of Object.values(mod))
76
- if (schema?.$id) app.addSchema(schema);
66
+ const mod = await import(pathToFileURL(file).href);
67
+ for (const [key, val] of Object.entries(mod)) {
68
+ if (key === "default" || key.startsWith("autoregister_")) continue;
69
+
70
+ if (globals.has(key)) {
71
+ console.error("\n❌ Global Identifier Conflict!");
72
+ console.error(`The export "${key}" is defined in multiple files:`);
73
+ console.error(` -> ${globals.get(key)}`);
74
+ console.error(` -> ${file}\n`);
75
+ throw new Error(`Duplicate global identifier "${key}"`);
76
+ }
77
+
78
+ globals.set(key, file);
79
+ global[key] = val;
77
80
  }
78
81
  }
79
82
 
80
- // 2. Pass Two: Auto-register hooks
83
+ // 2. Pass Two: Register Global Schemas (Lonely .schema.js files)
81
84
  for (const file of files) {
85
+ const isLonely = file.endsWith(".schema.js") &&
86
+ !fs.existsSync(file.replace(".schema.js", ".js"));
87
+
88
+ if (!isLonely) continue;
89
+
90
+ const mod = await import(pathToFileURL(file).href);
91
+ for (const schema of Object.values(mod))
92
+ if (schema?.$id) app.addSchema(schema);
93
+ }
94
+
95
+ // 3. Pass Three: Auto-register hooks
96
+ for (const file of files) {
97
+ if (file.endsWith(".schema.js")) continue;
98
+
82
99
  const mod = await import(pathToFileURL(file).href);
83
100
  for (const [key, fn] of Object.entries(mod))
84
101
  if (key.startsWith("autoregister_") && typeof fn === "function") await fn(app);
85
102
  }
86
103
 
87
- // 3. Pass Three: Register Routes
88
- const seen = new Map();
104
+ // 4. Pass Four: Collect and Group Routes by Directory
105
+ const groups = new Map();
89
106
  for (const file of files) {
90
107
  if (file.endsWith(".schema.js")) continue;
91
108
 
92
109
  const info = getRoute(file);
93
110
  if (!info) continue;
94
111
 
95
- const key = `${info.method} ${info.url}`;
96
- if (seen.has(key)) throw new Error(`Duplicate route "${key}" detected.`);
97
- seen.set(key, file);
98
-
99
- const handler = await importDefault(file);
112
+ const mod = await import(pathToFileURL(file).href);
113
+ const handler = mod.default;
100
114
  if (typeof handler !== "function") {
101
- throw new Error(`Route "${key}" in "${file}" must export a default function.`);
115
+ throw new Error(`Route in "${file}" must export a default function.`);
102
116
  }
103
117
 
104
118
  const schemaFile = file.replace(/\.js$/, ".schema.js");
105
119
  let raw = {};
106
120
 
107
121
  if (fs.existsSync(schemaFile)) {
108
- const loaded = await importDefault(schemaFile);
122
+ const loaded = (await import(pathToFileURL(schemaFile).href)).default;
109
123
  if (loaded && typeof loaded === "object") raw = loaded;
110
124
  }
111
125
 
112
- let { auth, routeOptions = {}, ...schema } = raw;
126
+ let { auth, order = 999, routeOptions = {}, ...schema } = raw;
113
127
 
114
- // Inject 'auth' config for the global authentication hook
115
128
  if (auth !== undefined) {
116
129
  routeOptions = {
117
130
  ...routeOptions,
@@ -119,6 +132,33 @@ export async function setupRoutes(app) {
119
132
  };
120
133
  }
121
134
 
122
- app[info.method](info.url, { ...routeOptions, schema }, handler);
135
+ const dir = path.dirname(file);
136
+ if (!groups.has(dir)) groups.set(dir, []);
137
+
138
+ groups.get(dir).push({
139
+ method: info.method,
140
+ url: info.url,
141
+ options: { ...routeOptions, schema },
142
+ handler,
143
+ order,
144
+ file
145
+ });
146
+ }
147
+
148
+ // Sort routes locally within each directory
149
+ const routes = [];
150
+ for (const list of groups.values()) {
151
+ list.sort((a, b) => a.order - b.order);
152
+ routes.push(...list);
153
+ }
154
+
155
+ // 5. Pass Five: Register Sorted Routes
156
+ const seen = new Map();
157
+ for (const r of routes) {
158
+ const key = `${r.method} ${r.url}`;
159
+ if (seen.has(key)) throw new Error(`Duplicate route "${key}" detected.`);
160
+ seen.set(key, r.file);
161
+
162
+ app[r.method](r.url, r.options, r.handler);
123
163
  }
124
164
  }
@@ -0,0 +1,2 @@
1
+ @node
2
+ @js
@@ -9,20 +9,5 @@
9
9
  },
10
10
  "type": "module",
11
11
  "private": "true",
12
- "dependencies": {},
13
- "keywords": [
14
- "fastify",
15
- "backend",
16
- "framework",
17
- "node-server",
18
- "api-server",
19
- "auto-routes",
20
- "automatic-docs",
21
- "scalar",
22
- "openapi",
23
- "jwt-auth",
24
- "mongodb",
25
- "minimalist",
26
- "zero-boilerplate"
27
- ]
12
+ "dependencies": {}
28
13
  }
@@ -0,0 +1,13 @@
1
+ export default {
2
+ $id: "User",
3
+ summary: "User Profile",
4
+ description: "Internal user profile including team memberships.",
5
+ tags: ["User"],
6
+ auth: true,
7
+ type: "object",
8
+ additionalProperties: false,
9
+ properties: {
10
+ id: { type: "string" },
11
+ email: { type: "string" },
12
+ }
13
+ };