xpref 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/README.md +282 -228
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,297 +1,394 @@
1
- # API Package
1
+ # xpref
2
2
 
3
- A powerful Express.js server setup package that provides a simple interface for creating and configuring Express applications with built-in security, logging, and routing features.
3
+ Express.js application bootstrap for APIs nested routing, request validation, OpenAPI docs, idempotency, proxying, and i18n.
4
4
 
5
5
  ## Features
6
6
 
7
- - Automatic port management with fallback
8
- - Built-in security with Helmet
9
- - CORS support
10
- - Request logging with log-client integration
11
- - Request ID tracking
12
- - Static file serving
13
- - Route registration
14
- - Custom middleware support
15
- - TypeScript support
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`)
16
68
 
17
69
  ## Usage
18
70
 
19
- ### Basic Server Setup
71
+ ### Basic server setup
20
72
 
21
73
  ```typescript
22
74
  import xpref from 'xpref';
23
75
 
24
- // Basic server setup
25
76
  xpref({
26
77
  appName: 'my-app',
27
78
  appEnv: 'development',
28
79
  port: 3000,
29
80
  routes: {
30
81
  '/api/users': [
31
- 'users', // route name
32
- [], // middleware array
82
+ 'users',
83
+ [],
33
84
  {
34
85
  get: (req, res) => {
35
86
  res.json({ users: [] });
36
- }
87
+ },
37
88
  },
38
- {} // children routes
39
- ]
40
- }
89
+ {},
90
+ ],
91
+ },
41
92
  }).then(({ port, app }) => {
42
93
  console.log(`Server running on port ${port}`);
43
94
  });
44
95
  ```
45
96
 
46
- ### With Request Logging
97
+ ### Nested routes and middleware
47
98
 
48
99
  ```typescript
49
- import xpref from 'xpref';
50
- import { createLogger } from '@core/log-client';
51
-
52
- // Create a logger instance
53
- const logger = createLogger({
54
- appName: 'my-app',
55
- appEnv: 'development'
56
- });
57
-
58
100
  xpref({
59
101
  appName: 'my-app',
60
102
  appEnv: 'development',
61
103
  port: 3000,
62
- logger, // Pass the logger instance
104
+ interceptor: (app) => {
105
+ app.use((req, res, next) => {
106
+ console.log('Custom middleware');
107
+ next();
108
+ });
109
+ },
63
110
  routes: {
64
- '/api/logs': [
65
- 'logs',
111
+ '/api/users': [
112
+ 'users',
66
113
  [],
67
114
  {
68
- get: (req, res) => {
69
- res.json({ logs: [] });
70
- }
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
+ ],
71
128
  },
72
- {}
73
- ]
74
- }
129
+ ],
130
+ },
75
131
  });
76
132
  ```
77
133
 
78
- ### With Multiple Routes and Methods
134
+ ### Request validation
79
135
 
80
136
  ```typescript
81
137
  xpref({
82
138
  appName: 'my-app',
83
139
  appEnv: 'development',
84
140
  port: 3000,
141
+ schemas: {
142
+ email: { type: 'string', format: 'email' },
143
+ name: { type: 'string', minLength: 1 },
144
+ },
85
145
  routes: {
86
146
  '/api/users': [
87
147
  'users',
88
- [], // middleware array
148
+ [],
89
149
  {
90
- get: (req, res) => {
91
- res.json({ users: [] });
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
+ },
92
162
  },
93
- post: (req, res) => {
94
- res.json({ message: 'User created' });
95
- }
96
163
  },
97
- {
98
- '/create': [
99
- 'create-user',
100
- [], // middleware array
101
- {
102
- post: (req, res) => {
103
- res.json({ message: 'User created' });
104
- }
105
- },
106
- {}
107
- ]
108
- }
109
164
  ],
110
- '/api/auth': [
111
- 'auth',
112
- [], // middleware array
113
- {
114
- post: (req, res) => {
115
- res.json({ token: 'jwt-token' });
116
- }
117
- },
118
- {}
119
- ]
120
- }
165
+ },
121
166
  });
122
167
  ```
123
168
 
124
- ### With Static Files
169
+ ### With request logging
125
170
 
126
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
+
127
180
  xpref({
128
181
  appName: 'my-app',
129
182
  appEnv: 'development',
130
183
  port: 3000,
131
- staticRoutes: {
132
- '/public': './public',
133
- '/uploads': './uploads'
134
- },
184
+ logger,
135
185
  routes: {
136
- '/api/files': [
137
- 'files',
186
+ '/api/logs': [
187
+ 'logs',
138
188
  [],
139
189
  {
140
- get: (req, res) => {
141
- res.json({ files: [] });
142
- },
143
- post: (req, res) => {
144
- res.json({ message: 'File uploaded' });
145
- },
146
- put: (req, res) => {
147
- res.json({ message: 'File updated' });
148
- },
149
- delete: (req, res) => {
150
- res.json({ message: 'File deleted' });
151
- }
190
+ get: (req, res) => res.json({ logs: [] }),
152
191
  },
153
- {}
154
- ]
155
- }
192
+ ],
193
+ },
156
194
  });
157
195
  ```
158
196
 
159
- ### With Custom Middleware
197
+ ### Static files
160
198
 
161
199
  ```typescript
162
200
  xpref({
163
201
  appName: 'my-app',
164
202
  appEnv: 'development',
165
203
  port: 3000,
166
- interceptor: (app) => {
167
- // Add custom middleware
168
- app.use((req, res, next) => {
169
- console.log('Custom middleware');
170
- next();
171
- });
204
+ staticRoutes: {
205
+ '/public': './public',
206
+ '/uploads': './uploads',
172
207
  },
173
208
  routes: {
174
- '/api/protected': [
175
- 'protected',
176
- [authMiddleware], // route-specific middleware
209
+ '/api/files': [
210
+ 'files',
211
+ [],
177
212
  {
178
- get: (req, res) => {
179
- res.json({ data: 'Protected route' });
180
- },
181
- post: (req, res) => {
182
- res.json({ data: 'Protected route created' });
183
- },
184
- put: (req, res) => {
185
- res.json({ data: 'Protected route updated' });
186
- },
187
- delete: (req, res) => {
188
- res.json({ data: 'Protected route deleted' });
189
- }
213
+ get: (req, res) => res.json({ files: [] }),
190
214
  },
191
- {}
192
- ]
193
- }
215
+ ],
216
+ },
194
217
  });
195
218
  ```
196
219
 
197
- ### With Manual Start Control
220
+ ### Manual start
198
221
 
199
222
  ```typescript
200
223
  xpref({
201
224
  appName: 'my-app',
202
225
  appEnv: 'development',
203
226
  port: 3000,
204
- manuallyStart: ({ app, port }) => {
205
- // Custom server start logic
206
- return new Promise((resolve) => {
227
+ manuallyStart: ({ app, port }) =>
228
+ new Promise((resolve) => {
207
229
  const server = app.listen(port, () => {
208
230
  resolve({ port, app, server });
209
231
  });
210
- });
211
- },
232
+ }),
212
233
  routes: {
213
234
  '/api/health': [
214
235
  'health',
215
236
  [],
216
237
  {
217
- get: (req, res) => {
218
- res.json({ status: 'ok' });
219
- }
238
+ get: (req, res) => res.json({ status: 'ok' }),
220
239
  },
221
- {}
222
- ]
223
- }
240
+ ],
241
+ },
224
242
  });
225
243
  ```
226
244
 
227
- ## API Reference
245
+ ### Idempotency
228
246
 
229
- ### `xpref(props: API): Promise<{ port: number, app: Application }>`
247
+ ```typescript
248
+ import type { Route } from 'xpref';
249
+ import idempotency from 'xpref/idempotency';
230
250
 
231
- Creates and configures an Express application.
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.
232
270
 
233
- #### Parameters
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
+ });
234
291
 
235
- - `props`: Configuration object
236
- - `appName`: Name of your application
237
- - `appEnv`: Environment (development, production, etc.)
238
- - `port`: Port number (default: 3000)
239
- - `routes`: Object mapping routes to their configurations
240
- - `staticRoutes`: Object mapping static file paths
241
- - `manuallyStart`: Function for custom server start logic
242
- - `interceptor`: Function to add custom middleware
243
- - `logger`: LogClient instance from @core/log-client package
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
+ ```
244
299
 
245
- ### Route Configuration
300
+ ### i18n
246
301
 
247
302
  ```typescript
248
- type Handler = {
249
- get?: (req: Request, res: Response, next: NextFunction) => void;
250
- post?: (req: Request, res: Response, next: NextFunction) => void;
251
- put?: (req: Request, res: Response, next: NextFunction) => void;
252
- delete?: (req: Request, res: Response, next: NextFunction) => void;
253
- patch?: (req: Request, res: Response, next: NextFunction) => void;
254
- };
255
-
256
- type RouteConfig = [
257
- string, // route name
258
- Array<(req: Request, res: Response, next: NextFunction) => void>, // middleware array
259
- Handler, // handler object with HTTP methods
260
- Record<string, RouteConfig> // children routes
261
- ];
303
+ import { i18n } from 'xpref';
262
304
 
263
- type Routes = {
264
- [path: string]: RouteConfig;
265
- };
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');
266
314
  ```
267
315
 
268
- ### Static Route Configuration
316
+ ### OpenAPI / Swagger UI
269
317
 
270
318
  ```typescript
271
- type StaticRoutes = {
272
- [path: string]: string; // Maps URL path to file system path
273
- };
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);
274
338
  ```
275
339
 
276
- ## Built-in Features
340
+ ## API reference
277
341
 
278
- ### Security
279
- - Helmet.js for security headers
280
- - CORS enabled
281
- - Trust proxy enabled
282
- - JSON body parsing (8MB limit)
283
- - URL-encoded body parsing
342
+ ### `xpref(props: Xpref): Promise<{ port: number; app: Application }>`
284
343
 
285
- ### Request Tracking
286
- - Unique request ID generation
287
- - Request logging with log-client integration
288
- - Request ID middleware
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 |
289
356
 
290
- ### Error Handling
291
- - Automatic port fallback if port is in use
292
- - Error handling for server startup
357
+ ### Route configuration
293
358
 
294
- ## Example Project Structure
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
295
392
 
296
393
  ```
297
394
  src/
@@ -300,54 +397,14 @@ src/
300
397
  │ └── auth.ts
301
398
  ├── middleware/
302
399
  │ └── auth.ts
400
+ ├── locales/
401
+ │ └── en.json
303
402
  ├── static/
304
403
  │ └── public/
305
404
  └── index.ts
306
405
  ```
307
406
 
308
407
  ```typescript
309
- // src/routes/users.ts
310
- export default {
311
- '/api/users': [
312
- 'users',
313
- [],
314
- {
315
- get: (req, res) => {
316
- res.json({ users: [] });
317
- },
318
- post: (req, res) => {
319
- res.json({ message: 'User created' });
320
- }
321
- },
322
- {
323
- '/create': [
324
- 'create-user',
325
- [],
326
- {
327
- post: (req, res) => {
328
- res.json({ message: 'User created' });
329
- }
330
- },
331
- {}
332
- ]
333
- }
334
- ]
335
- };
336
-
337
- // src/routes/auth.ts
338
- export default {
339
- '/api/auth': [
340
- 'auth',
341
- [],
342
- {
343
- post: (req, res) => {
344
- res.json({ token: 'jwt-token' });
345
- }
346
- },
347
- {}
348
- ]
349
- };
350
-
351
408
  // src/index.ts
352
409
  import xpref from 'xpref';
353
410
  import { createLogger } from '@core/log-client';
@@ -355,10 +412,9 @@ import userRoutes from './routes/users';
355
412
  import authRoutes from './routes/auth';
356
413
  import authMiddleware from './middleware/auth';
357
414
 
358
- // Create logger instance
359
415
  const logger = createLogger({
360
416
  appName: 'my-api',
361
- appEnv: process.env.NODE_ENV || 'development'
417
+ appEnv: process.env.NODE_ENV || 'development',
362
418
  });
363
419
 
364
420
  xpref({
@@ -366,17 +422,16 @@ xpref({
366
422
  appEnv: process.env.NODE_ENV || 'development',
367
423
  port: 3000,
368
424
  staticRoutes: {
369
- '/public': './static/public'
425
+ '/public': './static/public',
370
426
  },
371
427
  interceptor: (app) => {
372
- // Add authentication middleware
373
428
  app.use('/api/protected', authMiddleware);
374
429
  },
375
- logger, // Pass the logger instance
430
+ logger,
376
431
  routes: {
377
432
  ...userRoutes,
378
- ...authRoutes
379
- }
433
+ ...authRoutes,
434
+ },
380
435
  }).then(({ port }) => {
381
436
  console.log(`Server running on port ${port}`);
382
437
  });
@@ -384,5 +439,4 @@ xpref({
384
439
 
385
440
  ## License
386
441
 
387
- MIT
388
-
442
+ ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpref",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "repository": {