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 +442 -0
- package/debug.js +1 -1
- package/index.d.ts +2 -2
- package/package.json +10 -3
- package/types.d.ts +1 -1
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
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Xpref } from './types';
|
|
2
2
|
import { getReqId } from './request-log';
|
|
3
|
-
export default function api(pProps:
|
|
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.
|
|
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
|
|
28
|
+
export type Xpref = {
|
|
29
29
|
appName: string;
|
|
30
30
|
appEnv: string;
|
|
31
31
|
port?: number;
|