xpref 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,442 @@
1
+ # xpref
2
+
3
+ Express.js application bootstrap for APIs — nested routing, request validation, OpenAPI docs, idempotency, proxying, and i18n.
4
+
5
+ ## Features
6
+
7
+ ### Core server
8
+ - Express 5 app bootstrap with a single `xpref()` call
9
+ - Automatic port fallback when the configured port is in use
10
+ - Manual start control via `manuallyStart`
11
+ - `onInit` hook (runs before built-in middleware)
12
+ - `interceptor` hook (runs after built-in middleware, before routes)
13
+ - Startup banner with app name, environment, and port
14
+ - TypeScript-first types and exports
15
+
16
+ ### Security & request pipeline
17
+ - Helmet security headers
18
+ - CORS enabled
19
+ - Trust proxy enabled
20
+ - JSON body parsing (8MB limit)
21
+ - URL-encoded body parsing
22
+ - Unique request ID (`Request-Id` / `request-id`) on every request and response
23
+ - `getRequestId()` helper to read the current request ID
24
+
25
+ ### Routing
26
+ - Declarative nested route trees (parent path + children)
27
+ - Per-route middleware arrays
28
+ - HTTP methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`
29
+ - Handler as a function, array of functions, or `{ action, params, description }`
30
+ - Static file serving via `staticRoutes`
31
+
32
+ ### Request validation (AJV)
33
+ - Shared schemas via the top-level `schemas` option
34
+ - Per-method validation for `query`, `path` (`params`), and `body`
35
+ - Type coercion, `$data`, and `ajv-errors` support
36
+ - Human-readable field error messages (400 responses)
37
+
38
+ ### Request logging
39
+ - Morgan console access logs (app name, env, request ID, method, status, URL, timing, user-agent)
40
+ - Optional external logger integration (e.g. `@core/log-client`)
41
+ - Structured log payload: request ID, IP, country, language, device ID, origin, referer, and base64 body for non-GET
42
+
43
+ ### OpenAPI / Swagger (`xpref/api-docs`)
44
+ - OpenAPI 3.0 document generation from routes and schemas
45
+ - Swagger UI middleware (`swagger-ui-express`)
46
+ - Tags and external docs metadata
47
+ - Bearer (JWT) and API key security schemes
48
+
49
+ ### Idempotency (`xpref/idempotency`)
50
+ - POST-only idempotency via `idempotency-key` header (UUID v4)
51
+ - Optional enforcement per route
52
+ - Configurable TTL (default 5 minutes)
53
+ - In-progress (`202`), cached success replay, and expired (`410`) responses
54
+ - Optional `validateResponse` callback for custom success rules
55
+ - JSON and `x-www-form-urlencoded` bodies
56
+
57
+ ### Request forwarder / proxy (`xpref/request-forwarder`)
58
+ - `forwarder` — proxy HTTP/HTTPS upstream with body forwarding
59
+ - `proxy` — stream pipe to upstream
60
+ - Custom host, `proxyPrefix`, `withPrefix`, extra headers
61
+ - `onUrlConstructed` URL rewrite hook
62
+ - `passToNext` — collect upstream result and continue the middleware chain
63
+
64
+ ### Internationalization (`xpref/i18n`)
65
+ - Locale JSON files loaded at init
66
+ - `translate(key, replace?, lang?)` with `{placeholder}` substitution
67
+ - Fallback language support (`fallbackLang`, `fallbackLangOnly`)
68
+
69
+ ## Usage
70
+
71
+ ### Basic server setup
72
+
73
+ ```typescript
74
+ import xpref from 'xpref';
75
+
76
+ xpref({
77
+ appName: 'my-app',
78
+ appEnv: 'development',
79
+ port: 3000,
80
+ routes: {
81
+ '/api/users': [
82
+ 'users',
83
+ [],
84
+ {
85
+ get: (req, res) => {
86
+ res.json({ users: [] });
87
+ },
88
+ },
89
+ {},
90
+ ],
91
+ },
92
+ }).then(({ port, app }) => {
93
+ console.log(`Server running on port ${port}`);
94
+ });
95
+ ```
96
+
97
+ ### Nested routes and middleware
98
+
99
+ ```typescript
100
+ xpref({
101
+ appName: 'my-app',
102
+ appEnv: 'development',
103
+ port: 3000,
104
+ interceptor: (app) => {
105
+ app.use((req, res, next) => {
106
+ console.log('Custom middleware');
107
+ next();
108
+ });
109
+ },
110
+ routes: {
111
+ '/api/users': [
112
+ 'users',
113
+ [],
114
+ {
115
+ get: (req, res) => res.json({ users: [] }),
116
+ post: (req, res) => res.json({ message: 'User created' }),
117
+ },
118
+ {
119
+ '/:id': [
120
+ 'user-by-id',
121
+ [authMiddleware],
122
+ {
123
+ get: (req, res) => res.json({ id: req.params.id }),
124
+ put: (req, res) => res.json({ message: 'Updated' }),
125
+ delete: (req, res) => res.json({ message: 'Deleted' }),
126
+ },
127
+ ],
128
+ },
129
+ ],
130
+ },
131
+ });
132
+ ```
133
+
134
+ ### Request validation
135
+
136
+ ```typescript
137
+ xpref({
138
+ appName: 'my-app',
139
+ appEnv: 'development',
140
+ port: 3000,
141
+ schemas: {
142
+ email: { type: 'string', format: 'email' },
143
+ name: { type: 'string', minLength: 1 },
144
+ },
145
+ routes: {
146
+ '/api/users': [
147
+ 'users',
148
+ [],
149
+ {
150
+ post: {
151
+ description: 'Create a user',
152
+ params: {
153
+ body: {
154
+ type: 'object',
155
+ properties: ['email', 'name'],
156
+ required: ['email', 'name'],
157
+ },
158
+ },
159
+ action: (req, res) => {
160
+ res.json({ message: 'User created', ...req.body });
161
+ },
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ });
167
+ ```
168
+
169
+ ### With request logging
170
+
171
+ ```typescript
172
+ import xpref from 'xpref';
173
+ import { createLogger } from '@core/log-client';
174
+
175
+ const logger = createLogger({
176
+ appName: 'my-app',
177
+ appEnv: 'development',
178
+ });
179
+
180
+ xpref({
181
+ appName: 'my-app',
182
+ appEnv: 'development',
183
+ port: 3000,
184
+ logger,
185
+ routes: {
186
+ '/api/logs': [
187
+ 'logs',
188
+ [],
189
+ {
190
+ get: (req, res) => res.json({ logs: [] }),
191
+ },
192
+ ],
193
+ },
194
+ });
195
+ ```
196
+
197
+ ### Static files
198
+
199
+ ```typescript
200
+ xpref({
201
+ appName: 'my-app',
202
+ appEnv: 'development',
203
+ port: 3000,
204
+ staticRoutes: {
205
+ '/public': './public',
206
+ '/uploads': './uploads',
207
+ },
208
+ routes: {
209
+ '/api/files': [
210
+ 'files',
211
+ [],
212
+ {
213
+ get: (req, res) => res.json({ files: [] }),
214
+ },
215
+ ],
216
+ },
217
+ });
218
+ ```
219
+
220
+ ### Manual start
221
+
222
+ ```typescript
223
+ xpref({
224
+ appName: 'my-app',
225
+ appEnv: 'development',
226
+ port: 3000,
227
+ manuallyStart: ({ app, port }) =>
228
+ new Promise((resolve) => {
229
+ const server = app.listen(port, () => {
230
+ resolve({ port, app, server });
231
+ });
232
+ }),
233
+ routes: {
234
+ '/api/health': [
235
+ 'health',
236
+ [],
237
+ {
238
+ get: (req, res) => res.json({ status: 'ok' }),
239
+ },
240
+ ],
241
+ },
242
+ });
243
+ ```
244
+
245
+ ### Idempotency
246
+
247
+ ```typescript
248
+ import type { Route } from 'xpref';
249
+ import idempotency from 'xpref/idempotency';
250
+
251
+ const routes = {
252
+ '/wallets': [
253
+ 'wallets',
254
+ [],
255
+ {},
256
+ {
257
+ '/transfer': [
258
+ 'transfer',
259
+ [idempotency({ enforced: true, ttl: 300 })],
260
+ {
261
+ post: walletTransferAction,
262
+ },
263
+ ],
264
+ },
265
+ ],
266
+ } as Route;
267
+ ```
268
+
269
+ Clients send `idempotency-key: <uuid-v4>` on POST. Retry on `5xx`, `422`, `429`, and similar failures with exponential backoff.
270
+
271
+ ### Request forwarder / proxy
272
+
273
+ ```typescript
274
+ import { forwarder, proxy } from 'xpref/request-forwarder';
275
+
276
+ // As middleware: forward and respond from upstream
277
+ app.use('/upstream', forwarder({
278
+ host: 'https://api.example.com',
279
+ proxyPrefix: '/v1',
280
+ headers: { 'x-api-key': 'secret' },
281
+ }));
282
+
283
+ // Collect upstream result and continue the chain
284
+ app.use('/gateway', forwarder({
285
+ host: 'https://api.example.com',
286
+ passToNext: true,
287
+ }), (req, res) => {
288
+ // Upstream status/headers/body available via applyProxyResultToRequest
289
+ res.json({ ok: true });
290
+ });
291
+
292
+ // Stream pipe proxy
293
+ app.use('/proxy', proxy({
294
+ host: 'https://api.example.com',
295
+ withPrefix: true,
296
+ onUrlConstructed: (url) => url.replace(/\/+$/, ''),
297
+ }));
298
+ ```
299
+
300
+ ### i18n
301
+
302
+ ```typescript
303
+ import { i18n } from 'xpref';
304
+
305
+ const t = i18n({
306
+ locale: {
307
+ en: './locales/en.json',
308
+ km: './locales/km.json',
309
+ },
310
+ fallbackLang: 'en',
311
+ });
312
+
313
+ t('welcome.message', { name: 'Ada' }, 'en');
314
+ ```
315
+
316
+ ### OpenAPI / Swagger UI
317
+
318
+ ```typescript
319
+ import setupApiDocs from 'xpref/api-docs';
320
+
321
+ const [serve, setup] = setupApiDocs(
322
+ {
323
+ info: {
324
+ title: 'My API',
325
+ version: '1.0.0',
326
+ description: 'API documentation',
327
+ },
328
+ bearerAuth: true,
329
+ apiKeys: ['x-api-key'],
330
+ tags: {
331
+ users: { description: 'User endpoints' },
332
+ },
333
+ },
334
+ { routes, schemas },
335
+ );
336
+
337
+ app.use('/docs', serve, setup);
338
+ ```
339
+
340
+ ## API reference
341
+
342
+ ### `xpref(props: Xpref): Promise<{ port: number; app: Application }>`
343
+
344
+ | Option | Type | Description |
345
+ | --- | --- | --- |
346
+ | `appName` | `string` | Application name |
347
+ | `appEnv` | `string` | Environment (e.g. `development`, `production`) |
348
+ | `port` | `number` | Listen port (default `3000`; auto-increments if in use) |
349
+ | `routes` | `Route` | Nested route configuration |
350
+ | `schemas` | `Record<string, any>` | Shared AJV schemas for method validation |
351
+ | `staticRoutes` | `Record<string, string>` | URL path → filesystem path |
352
+ | `logger` | `any` | Optional logger factory used by request logging |
353
+ | `onInit` | `(app) => void` | Hook before built-in middleware |
354
+ | `interceptor` | `(app) => void` | Hook after built-in middleware |
355
+ | `manuallyStart` | `({ app, port }) => Promise` | Skip auto-listen; start the server yourself |
356
+
357
+ ### Route configuration
358
+
359
+ ```typescript
360
+ type MethodOptions =
361
+ | CallableFunction
362
+ | CallableFunction[]
363
+ | {
364
+ action: CallableFunction | CallableFunction[];
365
+ params?: { query?: any; path?: any; body?: any };
366
+ description?: string | [string, string];
367
+ };
368
+
369
+ type PathDetail = [
370
+ string, // route name
371
+ any[], // middleware
372
+ MethodHandler, // get/post/put/delete/patch
373
+ Record<string, PathDetail>?, // children
374
+ ];
375
+
376
+ type Route = Record<string, PathDetail>;
377
+ ```
378
+
379
+ ### Exports
380
+
381
+ | Export | From | Description |
382
+ | --- | --- | --- |
383
+ | default `xpref` | `xpref` | Create and start the app |
384
+ | `getRequestId` | `xpref` | Read request ID from a request |
385
+ | `i18n` | `xpref` | Initialize translations |
386
+ | Express types / `urlencoded` | `xpref` | Re-exported for convenience |
387
+ | `idempotency` | `xpref/idempotency` | Idempotency middleware |
388
+ | `setupApiDocs` | `xpref/api-docs` | OpenAPI + Swagger UI |
389
+ | `forwarder`, `proxy` | `xpref/request-forwarder` | Upstream proxy helpers |
390
+
391
+ ## Example project structure
392
+
393
+ ```
394
+ src/
395
+ ├── routes/
396
+ │ ├── users.ts
397
+ │ └── auth.ts
398
+ ├── middleware/
399
+ │ └── auth.ts
400
+ ├── locales/
401
+ │ └── en.json
402
+ ├── static/
403
+ │ └── public/
404
+ └── index.ts
405
+ ```
406
+
407
+ ```typescript
408
+ // src/index.ts
409
+ import xpref from 'xpref';
410
+ import { createLogger } from '@core/log-client';
411
+ import userRoutes from './routes/users';
412
+ import authRoutes from './routes/auth';
413
+ import authMiddleware from './middleware/auth';
414
+
415
+ const logger = createLogger({
416
+ appName: 'my-api',
417
+ appEnv: process.env.NODE_ENV || 'development',
418
+ });
419
+
420
+ xpref({
421
+ appName: 'my-api',
422
+ appEnv: process.env.NODE_ENV || 'development',
423
+ port: 3000,
424
+ staticRoutes: {
425
+ '/public': './static/public',
426
+ },
427
+ interceptor: (app) => {
428
+ app.use('/api/protected', authMiddleware);
429
+ },
430
+ logger,
431
+ routes: {
432
+ ...userRoutes,
433
+ ...authRoutes,
434
+ },
435
+ }).then(({ port }) => {
436
+ console.log(`Server running on port ${port}`);
437
+ });
438
+ ```
439
+
440
+ ## License
441
+
442
+ ISC
package/debug.js CHANGED
@@ -21,6 +21,6 @@ function getRaiser(error) {
21
21
  */
22
22
  function logDebug(error) {
23
23
  const res = getRaiser(error);
24
- const msg = { ...res, source: 'xpressify' };
24
+ const msg = { ...res, source: 'xpref' };
25
25
  console.log(msg);
26
26
  }
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { API } from './types';
1
+ import type { Xpref } from './types';
2
2
  import { getReqId } from './request-log';
3
- export default function api(pProps: API, tried?: number): any;
3
+ export default function api(pProps: Xpref, tried?: number): any;
4
4
  export { urlencoded, type Request, type Response, type NextFunction, type Application, } from 'express';
5
5
  export declare const getRequestId: typeof getReqId;
6
6
  export * from './types';
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "xpref",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/yarinnim/xpref.git"
9
+ },
6
10
  "scripts": {
7
11
  "build": "tsc --build -f ./tsconfig.json",
8
12
  "start:dev": "tsc --build -f ./tsconfig.json -w",
@@ -11,12 +15,15 @@
11
15
  "jsdoc": "tsc && jsdoc build/**/* -d jsdoc",
12
16
  "eslint": "eslint src --ext .ts"
13
17
  },
14
- "author": "",
18
+ "author": {
19
+ "name": "Yarin NIM <yarin.nim@gmail.com>",
20
+ "web": "https://github.com/yarinnim"
21
+ },
15
22
  "license": "ISC",
23
+ "sideEffects": false,
16
24
  "publishConfig": {
17
25
  "access": "public"
18
26
  },
19
- "sideEffects": false,
20
27
  "dependencies": {
21
28
  "ajv": "^8.17.1",
22
29
  "ajv-errors": "^3.0.0",
package/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export type PathMiddleware = Array<any>;
25
25
  export type PathDetail = [string, PathMiddleware, MethodHandler] | [string, PathMiddleware, MethodHandler, Record<string, PathDetail>];
26
26
  export type Route = Record<any, PathDetail>;
27
27
  type OnInit = (_app: Application) => void;
28
- export type API = {
28
+ export type Xpref = {
29
29
  appName: string;
30
30
  appEnv: string;
31
31
  port?: number;