maxserver 0.0.14 → 0.0.16

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
@@ -11,63 +11,63 @@
11
11
  I am simplifying and improving things, that it will work for everyone plugn play.
12
12
 
13
13
 
14
- Ready node server setup based on **Fastify** to speedup api development.
14
+ Ready node server setup based on **Fastify** to speed up backend development.
15
+ maxserver stands for **maximized simplicity** and **minimum boilerplate**.
15
16
 
16
17
  - **Auto Routes**: auto imports and registers routes and schemas
17
18
  - **Auto Docs**: auto generates docs based on schemas
18
- - **Preconfigures JWT auth, Cores, Helmet**
19
+ - **Preconfigures essentials**: jwt auth, cors, helmet
19
20
  - **Auto Connect MongoDB** (optional)
20
21
  - **Dev server**
21
22
  <br><br>
22
23
 
23
24
 
24
- - Dependencies: original fastify packages + scalar/fastify-api-reference (doc generator)
25
- - The source is simple and short. Everyone shall be able to read, understand and modify if needed.
26
-
27
-
28
25
  ## Install
29
26
 
30
27
  ### Setup ready project
28
+ ```js
31
29
  npx maxserver [appname]
30
+ ```
32
31
 
33
- ### Install
32
+ ### Or install as packge
33
+ ```js
34
34
  npm install maxserver
35
+ ```
36
+ <br>
35
37
 
36
38
  ## Setup
37
39
  ```js
38
40
  import maxserver from "maxserver";
39
- const server = await maxserver();
40
- const address = await server.listen({
41
- port: Number(process.env.PORT || 3000),
41
+
42
+ const server = await maxserver({
43
+ port: 3000,
44
+ secret: "your_secret"
42
45
  });
43
46
 
44
- console.log("Server running at", address);
47
+ await server.start();
45
48
  export default server;
46
49
  ```
50
+ <br>
47
51
 
48
- **maxserver(options)** forwards options to fastify(options).
49
- It returns the fully configured Fastify server instance.
50
-
51
-
52
- ---
52
+ ## ⚙️ Configure
53
+ Configs can be passed to the init call to **maxserver()** or set in your .env file.
54
+ If you define options in env, use all upper case letters.
55
+ Any fastify options can be passed to maxserver() too.
53
56
 
54
- ## ⚙️ Configuration
55
- Configure the server by setting variabels in your .env file.
56
57
 
57
58
  | Variable | Default | Description |
58
59
  | :--- | :--- | :--- |
59
- | `PORT` | `3000` | Server port |
60
- | `JWT_SECRET` | *-* | Enables JWT auth (private-by-default routes) |
61
- | `MONGODB_URI` | *-* | Enables MongoDB auto-connect + global `db` |
62
- | `DOCS` | `true` | Set `false` to disable docs UI at `/docs` |
63
- | `STATIC_DIR` | *(optional)* | Serve static files (example: `./public`) |
64
- | `CORS_ORIGIN` | `*` | Allowed CORS origins |
65
-
66
- ---
67
-
68
- ## 🗂️ Project Structure
60
+ | `port` | `3000` | Server port |
61
+ | `secret` | *-* | Secret used for jwt and cookies |
62
+ | `cors` | `*` | Allowed CORS origins, default all allowed |
63
+ | `docs` | `true` | Set `false` to disable auto generated docs` |
64
+ | `mongodb` | *-* | MongoDB URI, if set auto-connects db |
65
+ | `public` | `false` | Set `true` to expose the server publicly (binds to `0.0.0.0`) |
66
+ | `static` | *-* | If set, serves this directory statically |
67
+ <br><br>
69
68
 
70
- **1 route = 1 handler file + 1 schema file**
69
+ ## 🗂️ Project Structure
70
+ Our golden rule: **1 route = 1 handler file + 1 schema file**
71
71
 
72
72
  Example:
73
73
 
@@ -80,12 +80,11 @@ src/
80
80
  hello.schema.js
81
81
  ...
82
82
  ```
83
-
84
- ---
83
+ <br>
85
84
 
86
85
  ## 🛣️ Handlers
87
86
 
88
- ### 1) Define method + path
87
+ #### 1) Define method + path
89
88
  Start each route file with a comment to define the path.
90
89
  That comment is what the route loader uses to auto-register the route.
91
90
 
@@ -93,13 +92,9 @@ That comment is what the route loader uses to auto-register the route.
93
92
  // GET /teams/:id
94
93
  ```
95
94
 
95
+ #### 2) Export default handler
96
96
 
97
97
 
98
- ### 2) Default export handler
99
-
100
-
101
- ### Example
102
-
103
98
  ```js
104
99
  // GET /teams/:id
105
100
 
@@ -111,89 +106,75 @@ export default async function (req, res) {
111
106
  }
112
107
  ```
113
108
 
114
- ---
109
+ <br>
110
+ <br>
111
+
115
112
 
116
- ## 🧾 Define Schemas
117
- Create a sibling file ending in **`.schema.js`**.
118
- The schema is offcourse a jsonschema.
119
- This file will be auto registered.
113
+ ## 🧾 Schemas
114
+ Create a sibling file ending with **`.schema.js`**, so it will be auto registered.
115
+ For example: **hello.js** and **hello.schema.js**
120
116
 
121
- Schemas:
122
- - validate inputs
123
- - generate OpenAPI docs
124
- - control route options for example (public/private)
117
+ Besides the basic validation fields we can set fields like summary and description,
118
+ which will appear in the docs. Mostly you don't need to write schemas yourself, chat gpt and gemini do it excelently.
125
119
 
126
120
 
127
- **`Important - use default export`**
128
121
  ```js
129
122
  export default {
130
- tags: ["Teams"],
131
- summary: "Get team",
132
- description: "Returns a single team by identifier.",
133
123
 
134
- params: {
124
+ tags: ["Test"],
125
+ summary: "Post hello",
126
+ description: "Accepts a name and returns a greeting.",
127
+
128
+ body: {
135
129
  type: "object",
136
- required: ["id"],
130
+ required: ["name"],
137
131
  properties: {
138
- id: {
132
+ name: {
139
133
  type: "string",
140
- minLength: 24,
141
- example: "",
142
134
  },
143
135
  },
144
136
  },
145
137
 
146
- response: {
147
- 200: {
148
- type: "object",
149
- properties: {
150
- id: {
151
- type: "string",
152
- example: "507f1f77bcf86cd799439011",
153
- },
154
- name: { type: "string", example: "Team A" },
155
- },
156
- required: ["id", "name"],
157
- },
158
- },
138
+ ...
159
139
  };
160
140
  ```
161
141
 
162
- <br>
163
-
164
-
165
- ## 📚 API Docs
166
-
167
- - Open in your browser **`localhost:3000/docs`**
168
- - OpenAPI JSON: **`/openapi.json`**
142
+ **`‼️ Important use export default`**
169
143
 
170
144
  <br>
171
145
 
146
+ ## Route Options
147
+ Though we don't register routes manually, we don't set route options on the register call. If needed, route options can be set on the schema object.
148
+ For example **schema.auth = true** to enable authentication.
149
+ So the schema holds all configs about a root and handlers the pure logic.
172
150
 
151
+ <br>
173
152
 
153
+ ## 📚 API Docs
174
154
 
175
- ## 🔐 Authentication (JWT)
155
+ Open in your browser **`localhost:3000/docs`**
156
+ OpenAPI JSON: **`/openapi.json`**
176
157
 
177
- Enable auth by setting:
158
+ <br>
178
159
 
179
- - `JWT_SECRET`
180
160
 
181
- Behavior:
182
- - **Private by default**: routes require JWT (cookie or Bearer header)
183
- - Authenticated user identifier is available as **`req.userId`**
184
- - Make a route **public** by setting `config.public: true` in its schema
161
+ ## 🔐 Authentication
162
+ JWT header and cookie based auth is preconfigured.
163
+ To enable auth for a route set in it's schema **auth = true**
164
+ The authenticated user is available as **`req.userId`**
185
165
 
186
166
  ```js
167
+ // Inside schema
168
+
187
169
  export default {
188
- config: { public: true },
189
- // ...
170
+ auth: true
190
171
  };
191
172
  ```
192
173
 
193
174
  <br>
194
175
 
195
176
  ## 🍃 MongoDB
196
- Define in the env **`MONGODB_URI`** and it will auto-connect at server start and you get:
177
+ Set option **`MONGODB`** your mongodbURI and it will auto-connect at server start and you get:
197
178
 
198
179
  - global **`db`** (connected database handle)
199
180
  - global **`oid(string)`** (string → MongoDB `ObjectId`)
@@ -203,8 +184,19 @@ Define in the env **`MONGODB_URI`** and it will auto-connect at server start and
203
184
  | `db` | MongoDB database handle | Use it directly in handlers |
204
185
  | `oid(id)` | string → `ObjectId` | Avoid importing `ObjectId` everywhere |
205
186
 
187
+ ### Example
188
+
189
+ ```js
190
+ // Inside route handlers
191
+
192
+ export default async function (req, res) {
193
+
194
+ await db.feedback.insert(...)
195
+ }
196
+ ```
197
+
206
198
 
207
- ---
199
+ <br>
208
200
 
209
201
  ## 🧰 Error Handling
210
202
 
@@ -216,9 +208,13 @@ if (!user) throw createError(404, "User not found");
216
208
 
217
209
  Rule of thumb: make the message something you would want to see at 03:00 in logs.
218
210
 
219
- ---
211
+ <br>
220
212
 
213
+ ## About
214
+ - Dependencies: original fastify packages + scalar/fastify-api-reference
215
+ - The source is simple. Everyone can read, understand and modify if needed.
221
216
 
217
+ <br>
222
218
 
223
219
  ## 🛠️ Tips & Tools
224
220
 
@@ -235,5 +231,6 @@ In `.vscode/tasks.json`, enable the task with:
235
231
  "runOptions": { "runOn": "folderOpen" }
236
232
  ```
237
233
 
238
- ### 🤖 AI Assistants (Code Style)
239
- Copy **`RULES.md`** into your AI tool as system context, then ask it to generate routes + schemas.
234
+ ### 🤖 AI Assistants
235
+ Copy **`RULES.md`** into your AI tool as system context,
236
+ then ask it to generate routes + schemas.
package/bin/init.js CHANGED
@@ -15,17 +15,17 @@ function main() {
15
15
  }
16
16
 
17
17
  try {
18
- console.log(`🚀 Creating "${projectName}"...`);
18
+ console.log(`🚀 Setting up "${projectName}"...`);
19
19
  fs.cpSync(templateDir, targetDir, { recursive: true });
20
20
 
21
21
  fixDotfiles(targetDir);
22
22
  patchPackageJson(targetDir, projectName);
23
23
 
24
24
  process.chdir(targetDir);
25
- console.log("📦 Installing maxserver...");
25
+ console.log("📦 Installing maxserver");
26
26
  execSync("npm install maxserver@latest", { stdio: "inherit" });
27
27
 
28
- console.log("\n✅ Done! Your project is ready. 😊");
28
+ console.log(`\n✅ Install complete\n\ncd ${projectName}\nnpm run dev\n`);
29
29
  } catch (err) {
30
30
  console.error("❌ Init failed:", err.message);
31
31
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxserver",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Node server setup based fastify",
5
5
  "author": "Max Matinpalo",
6
6
  "type": "module",
package/src/getAddress.js CHANGED
@@ -1,29 +1,5 @@
1
1
  import os from "node:os";
2
2
 
3
- export function setupGetAddress(app) {
4
- app.decorate("getAddress", () => {
5
- const addr = app.server?.address();
6
- const protocol = app.initialConfig?.https ? "https" : "http";
7
-
8
- if (!addr) return null;
9
- if (typeof addr === "string") return addr;
10
-
11
- const isPublicBind = addr.address === "0.0.0.0" || addr.address === "::";
12
- const isLoopback = addr.address === "127.0.0.1" || addr.address === "::1";
13
-
14
- const envIp = String(process.env.PUBLIC_IP || "").trim() || null;
15
- const detectedIp = envIp || getLanIp();
16
-
17
- const ip = (isPublicBind && !isLoopback)
18
- ? (detectedIp || addr.address)
19
- : "localhost";
20
-
21
- const host = ip.includes(":") ? `[${ip}]` : ip;
22
-
23
- return `${protocol}://${host}:${addr.port}`;
24
- });
25
- }
26
-
27
3
  function getLanIp() {
28
4
  const nets = os.networkInterfaces();
29
5
 
@@ -35,3 +11,28 @@ function getLanIp() {
35
11
 
36
12
  return null;
37
13
  }
14
+
15
+
16
+ export function getAddress(app) {
17
+
18
+ const addr = app.server?.address();
19
+ const protocol = app.initialConfig?.https ? "https" : "http";
20
+
21
+ if (!addr) return null;
22
+ if (typeof addr === "string") return addr;
23
+
24
+ const isPublicBind = addr.address === "0.0.0.0" || addr.address === "::";
25
+ const isLoopback = addr.address === "127.0.0.1" || addr.address === "::1";
26
+
27
+ const envIp = String(process.env.PUBLIC_IP || "").trim() || null;
28
+ const detectedIp = envIp || getLanIp();
29
+
30
+ const ip = (isPublicBind && !isLoopback)
31
+ ? (detectedIp || addr.address)
32
+ : "localhost";
33
+
34
+ const host = ip.includes(":") ? `[${ip}]` : ip;
35
+
36
+ return `${protocol}://${host}:${addr.port}`;
37
+ }
38
+
package/src/index.js CHANGED
@@ -12,29 +12,60 @@ import {
12
12
  getHttpsOptions,
13
13
  } from "./setup.js";
14
14
 
15
- import { setupGetAddress } from "./getAddress.js";
15
+ import { getAddress } from "./getAddress.js";
16
16
 
17
17
 
18
+ export default async function maxserver(config = {}) {
18
19
 
19
- export default async function maxserver(options = {}) {
20
+ const {
20
21
 
21
- const app = Fastify({
22
- trustProxy: true,
23
- host: "0.0.0.0",
24
- https: getHttpsOptions() || undefined,
22
+ // maxserver options
23
+ port = Number(process.env.PORT || 3000),
24
+ secret = process.env.SECRET,
25
+ mongodb = process.env.MONGODB,
26
+ docs = process.env.DOCS !== "false",
27
+ cors = process.env.CORS || "*",
28
+ openapiInfo,
29
+ static: isStatic = process.env.STATIC,
30
+ public: isPublic = process.env.PUBLIC === "true",
25
31
 
26
- // To allow writing example value fields to schemas for doucumentation
27
- ajv: { customOptions: { strictSchema: false } },
28
- ...options,
29
- });
32
+ // everything else goes straight to Fastify
33
+ ...fastifyOpts
30
34
 
31
- global.createError = function (code, message) {
32
- const err = new Error(message);
33
- err.statusCode = code;
34
- return err;
35
+ } = config;
36
+
37
+ const maxserverConfig = {
38
+ port, secret, mongodb, docs, cors,
39
+ static: isStatic,
40
+ public: isPublic
35
41
  };
36
42
 
37
- setupGetAddress(app);
43
+
44
+ let app;
45
+ try {
46
+ app = Fastify({
47
+ https: getHttpsOptions() || undefined,
48
+ trustProxy: true,
49
+
50
+ // Required to allow adding doc fields on schema
51
+ ajv: { customOptions: { strictSchema: false } },
52
+ ...fastifyOpts
53
+ });
54
+ } catch (err) {
55
+ console.error("❌ Fastify initialization failed:", err);
56
+ throw err;
57
+ }
58
+
59
+
60
+ app.decorate("maxserver", maxserverConfig);
61
+
62
+ app.decorate("start", async function () {
63
+ const port = this.maxserver.port ?? 3000;
64
+ const host = this.maxserver.public ? '0.0.0.0' : '127.0.0.1';
65
+ await this.listen({ port, host });
66
+ console.log('Server running at ', getAddress(this));
67
+ });
68
+
38
69
  await setupCookie(app);
39
70
  await setupHelmet(app);
40
71
  await setupCors(app);
@@ -44,6 +75,11 @@ export default async function maxserver(options = {}) {
44
75
  await setupDocs(app);
45
76
  await setupRoutes(app);
46
77
 
78
+ global.createError = function (code, message) {
79
+ const err = new Error(message);
80
+ err.statusCode = code;
81
+ return err;
82
+ };
47
83
 
48
84
  return app;
49
85
  }
package/src/setup.js CHANGED
@@ -12,20 +12,11 @@ import apiReference from "@scalar/fastify-api-reference";
12
12
 
13
13
  import { loadRoutes } from "./routeLoader.js";
14
14
 
15
-
16
- export async function setupCookie(app) {
17
- // Use COOKIE_SECRET or fall back to JWT_SECRET so you don't need a new env var immediately
18
- const secret = process.env.COOKIE_SECRET || process.env.JWT_SECRET || "change-me-in-prod";
19
-
20
- await app.register(cookie, {
21
- secret,
22
- hook: "onRequest", // Crucial: Ensures cookies are parsed before your route handlers run
23
- parseOptions: {}
24
- });
15
+ export async function setupRoutes(app) {
16
+ await loadRoutes(app);
25
17
  }
26
18
 
27
19
 
28
-
29
20
  export async function setupHelmet(app) {
30
21
  await app.register(helmet, {
31
22
  contentSecurityPolicy: false,
@@ -35,21 +26,22 @@ export async function setupHelmet(app) {
35
26
  });
36
27
  }
37
28
 
29
+
30
+
31
+
38
32
  export async function setupCors(app) {
39
33
  const isProd = process.env.NODE_ENV === "production";
40
- const origin = process.env.CORS_ORIGIN || true;
34
+ const origin = app.maxserver.cors ?? "*";
41
35
 
42
- if (isProd && !process.env.CORS_ORIGIN) app.log.warn("CORS_ORIGIN not set, allowing all origins");
36
+ if (isProd && origin === "*") {
37
+ app.log.warn("CORS: allowing all origins (*) in production");
38
+ }
43
39
 
44
40
  await app.register(cors, { origin });
45
41
  }
46
42
 
47
43
 
48
44
 
49
- export async function setupRoutes(app) {
50
- await loadRoutes(app);
51
- }
52
-
53
45
  export function getHttpsOptions() {
54
46
  const { TLS_KEY, TLS_CERT } = process.env;
55
47
  if (!TLS_KEY || !TLS_CERT) return null;
@@ -67,67 +59,63 @@ export function getHttpsOptions() {
67
59
 
68
60
 
69
61
 
70
-
71
- function isAuthSkippableUrl(url) {
72
- if (!url) return false;
73
- if (url.startsWith("/openapi.json")) return true;
74
- if (url.startsWith("/docs")) return true;
75
- if (url.startsWith("/static/")) return true;
76
- return false;
62
+ export async function setupCookie(app) {
63
+ await app.register(cookie, {
64
+ secret: app.maxserver.secret || "supersecret",
65
+ hook: "onRequest"
66
+ });
77
67
  }
78
68
 
69
+
79
70
  export async function setupJwt(app) {
80
- const secret = process.env.JWT_SECRET;
81
- if (!secret) return;
82
71
 
83
- // because we added own cookie setup function above
84
- //await app.register(cookie);
72
+ await app.register(jwt, {
73
+ secret: app.maxserver.secret || "supersecret",
74
+ cookie: { cookieName: "token" }
75
+ });
85
76
 
86
- await app.register(jwt, { secret, cookie: { cookieName: "token" } });
77
+ app.addHook("preHandler", async function (req) {
87
78
 
88
- app.addHook("onRequest", async function (req) {
79
+ // Let preflight requests pass
89
80
  if (req.method === "OPTIONS") return;
90
81
 
91
- const url = req.raw?.url || req.url;
92
- if (isAuthSkippableUrl(url)) return;
93
- if (req.routeOptions?.config?.public) return;
82
+ const auth =
83
+ req.routeOptions?.config?.auth ??
84
+ req.routeOptions?.schema?.auth;
94
85
 
95
- await req.jwtVerify();
86
+ if (!auth) return;
96
87
 
88
+ await req.jwtVerify();
97
89
  const u = req.user;
98
90
  req.userId = u?.sub || u?.userId || u?.userid || u?.id || null;
99
91
  });
92
+
100
93
  }
101
94
 
102
- export async function setupMongo(app) {
103
95
 
104
- const url = process.env.MONGODB_URI;
96
+ export async function setupMongo(app) {
97
+ const url = app.maxserver.mongodb;
105
98
  if (!url) return;
106
-
107
99
  await app.register(mongodb, { url });
108
100
 
109
- // ObjectId is available on app.mongo after registration
110
101
  const { ObjectId, db } = app.mongo;
111
-
112
102
  global.oid = id => new ObjectId(id ? String(id) : undefined);
113
103
  global.db = db;
114
104
  }
115
105
 
116
- export async function setupStatic(app) {
117
- const dir = process.env.STATIC_DIR;
118
- if (!dir) return;
119
-
120
- await app.register(fastifyStatic, {
121
- root: path.resolve(dir),
122
- prefix: "/static/",
123
- });
124
- }
125
106
 
126
107
  export async function setupDocs(app) {
127
- const defaultSecurity = [{ bearerAuth: [] }, { cookieAuth: [] }];
108
+
109
+ const info = app.maxserver.openapiInfo || {
110
+ title: "API",
111
+ version: "1.0.0",
112
+ };
128
113
 
129
114
  await app.register(swagger, {
130
115
  openapi: {
116
+ info,
117
+ // OpenAPI 3.x: securitySchemes must be defined globally here not per route
118
+ // Routes only add `security: [...]` that references these scheme names
131
119
  components: {
132
120
  securitySchemes: {
133
121
  bearerAuth: { type: "http", scheme: "bearer" },
@@ -137,13 +125,29 @@ export async function setupDocs(app) {
137
125
  },
138
126
  });
139
127
 
140
- app.get("/openapi.json", { config: { public: true } }, () => app.swagger());
141
128
 
142
- await app.register(apiReference, { routePrefix: "/docs", openapi: true });
129
+ app.get("/openapi.json", {}, () => app.swagger());
130
+
131
+ if (app.maxserver.docs != false)
132
+ await app.register(apiReference, { routePrefix: "/docs", openapi: true });
143
133
 
144
134
  app.addHook("onRoute", (route) => {
145
- if (route.config?.public) return;
135
+ const auth = route.config?.auth ?? route.schema?.auth;
136
+ if (!auth) return;
146
137
  route.schema ||= {};
147
- route.schema.security = defaultSecurity;
138
+ route.schema.security ||= [{ bearerAuth: [] }, { cookieAuth: [] }];
148
139
  });
149
- }
140
+ }
141
+
142
+
143
+
144
+ export async function setupStatic(app) {
145
+ const dir = app.maxserver.static;
146
+ if (!dir) return;
147
+
148
+ await app.register(fastifyStatic, {
149
+ root: path.resolve(dir),
150
+ prefix: "/static/",
151
+ });
152
+ }
153
+
package/templates/env CHANGED
@@ -1,24 +1,8 @@
1
- # Environment
2
- NODE_ENV = development
3
-
4
- # Server
5
- PORT = 3000
6
-
7
- # CORS(prod only, ignored in dev)
8
- # CORS_ORIGIN = https://app.example.com
9
-
10
- # JWT;
11
- # JWT_SECRET = supersecretkey
12
1
 
13
- # MongoDB;
14
- # MONGODB_URI = mongodb://127.0.0.1:27017/testdb
15
-
16
- # Static files
17
- # STATIC_DIR =./ public
18
-
19
- # API Docs in production
20
- DOCS = 1
2
+ NODE_ENV = development
21
3
 
22
- # Optional HTTPS(direct TLS, no nginx)
23
- # TLS_KEY = /etc/ssl / private / server.key
24
- # TLS_CERT = /etc/ssl / certs / server.crt;
4
+ # PORT
5
+ # CORS
6
+ # SECRET
7
+ # MONGODB
8
+ # STATIC
@@ -1,10 +1,9 @@
1
1
  import maxserver from "maxserver";
2
2
 
3
- const server = await maxserver();
4
-
5
- await server.listen({
6
- port: Number(process.env.PORT || 3000),
3
+ const server = await maxserver({
4
+ port: 3000,
5
+ secret: "your_secret"
7
6
  });
8
7
 
9
- console.log("Server running at ", server.getAddress());
8
+ await server.start();
10
9
  export default server;