xpref 1.0.3 → 1.0.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/package.json +1 -1
- package/request-forwarder/forwarder.d.ts +1 -1
- package/request-forwarder/pass-to-next.d.ts +3 -0
- package/request-forwarder/pass-to-next.js +3 -2
- package/request-forwarder/proxy.d.ts +1 -1
- package/seo/index.d.ts +2 -0
- package/seo/index.js +8 -0
- package/seo/seo.middleware.d.ts +9 -0
- package/seo/seo.middleware.js +69 -0
- package/seo/type.d.ts +12 -0
- package/seo/type.js +2 -0
- package/README.md +0 -442
package/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RequestForwarder, Response, Request, NextFunction } from '../types';
|
|
2
|
-
export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response, next: NextFunction) => import("
|
|
2
|
+
export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response, next: NextFunction) => import("http").ClientRequest;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ProxyResult, Request, Response } from '../types';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
+
type ProxyBody = string | Buffer | Record<string, unknown>;
|
|
4
|
+
export declare const parseProxyBody: (body: Buffer, headers: IncomingMessage['headers']) => ProxyBody;
|
|
3
5
|
export declare const applyProxyResultToRequest: (req: Request, res: Response, result: ProxyResult) => void;
|
|
4
6
|
export declare const collectProxyResult: (proxyResponse: IncomingMessage) => Promise<ProxyResult>;
|
|
7
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.collectProxyResult = exports.applyProxyResultToRequest = void 0;
|
|
3
|
+
exports.collectProxyResult = exports.applyProxyResultToRequest = exports.parseProxyBody = void 0;
|
|
4
4
|
const normalizeProxyHeaders = (headers) => (Object.entries(headers).reduce((acc, [key, value]) => {
|
|
5
5
|
if (value === undefined)
|
|
6
6
|
return acc;
|
|
@@ -22,9 +22,10 @@ const parseProxyBody = (body, headers) => {
|
|
|
22
22
|
return body.toString('utf8');
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
+
exports.parseProxyBody = parseProxyBody;
|
|
25
26
|
const applyProxyResultToRequest = (req, res, result) => {
|
|
26
27
|
const upstreamHeaders = normalizeProxyHeaders(result.headers);
|
|
27
|
-
req.body = parseProxyBody(result.body, result.headers);
|
|
28
|
+
req.body = (0, exports.parseProxyBody)(result.body, result.headers);
|
|
28
29
|
req.headers = { ...req.headers, ...upstreamHeaders };
|
|
29
30
|
res.statusCode = result.statusCode;
|
|
30
31
|
Object.entries(upstreamHeaders).forEach(([key, value]) => {
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* with it's prototol (http or https).
|
|
6
6
|
*/
|
|
7
7
|
import type { Request, Response, RequestForwarder } from '../types';
|
|
8
|
-
export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response) => import("
|
|
8
|
+
export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response) => import("http").ClientRequest;
|
package/seo/index.d.ts
ADDED
package/seo/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.seoMiddleware = void 0;
|
|
7
|
+
var seo_middleware_1 = require("./seo.middleware");
|
|
8
|
+
Object.defineProperty(exports, "seoMiddleware", { enumerable: true, get: function () { return __importDefault(seo_middleware_1).default; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from '../types';
|
|
2
|
+
import type { SeoProps } from './type';
|
|
3
|
+
type SeoProxyProps = {
|
|
4
|
+
siteName: string;
|
|
5
|
+
templatePath?: string;
|
|
6
|
+
};
|
|
7
|
+
type CallbackData = (data: any, req?: Request) => SeoProps | Promise<SeoProps>;
|
|
8
|
+
export default function prepareTemplate(props: SeoProxyProps, onData: CallbackData): (req: Request, res: Response, next: NextFunction) => void | Promise<void | Response<any, Record<string, any>>> | Response<any, Record<string, any>>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = prepareTemplate;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const readResPayload = (req) => {
|
|
9
|
+
const payload = req.body;
|
|
10
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
11
|
+
throw new Error('Invalid Application Request');
|
|
12
|
+
}
|
|
13
|
+
return payload;
|
|
14
|
+
};
|
|
15
|
+
const isValidCrawler = (req) => {
|
|
16
|
+
const strBool = String(req.headers['is-certified-crawler'] || '');
|
|
17
|
+
return strBool.trim() === 'true';
|
|
18
|
+
};
|
|
19
|
+
const getTemplatePath = (props) => {
|
|
20
|
+
const templatePath = props.templatePath || '';
|
|
21
|
+
if (templatePath !== '')
|
|
22
|
+
return templatePath;
|
|
23
|
+
return path_1.default.join(__dirname, 'seo.template.ejs');
|
|
24
|
+
};
|
|
25
|
+
const getSeoProps = (req, dataHandler) => {
|
|
26
|
+
const data = readResPayload(req);
|
|
27
|
+
if (!(data || false))
|
|
28
|
+
throw new Error('Invalid Response.');
|
|
29
|
+
const result = dataHandler(data, req);
|
|
30
|
+
if (result instanceof Promise)
|
|
31
|
+
return result.then((value) => value);
|
|
32
|
+
return Promise.resolve(result);
|
|
33
|
+
};
|
|
34
|
+
function prepareTemplate(props, onData) {
|
|
35
|
+
return (req, res, next) => {
|
|
36
|
+
var _a;
|
|
37
|
+
if (req.method === 'OPTIONS')
|
|
38
|
+
return next();
|
|
39
|
+
if (req.method !== 'GET')
|
|
40
|
+
return next();
|
|
41
|
+
if (!isValidCrawler(req))
|
|
42
|
+
return next();
|
|
43
|
+
const statusCode = Number(res.statusCode || 200);
|
|
44
|
+
if (statusCode >= 400) {
|
|
45
|
+
const message = ((_a = req.body) === null || _a === void 0 ? void 0 : _a.message)
|
|
46
|
+
|| 'Something goes wrong.';
|
|
47
|
+
return res.status(statusCode).json({ message });
|
|
48
|
+
}
|
|
49
|
+
return getSeoProps(req, onData)
|
|
50
|
+
.then((payload) => {
|
|
51
|
+
const templatePath = getTemplatePath(props);
|
|
52
|
+
res.removeHeader('content-type');
|
|
53
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
54
|
+
return res.status(200).render(templatePath, {
|
|
55
|
+
title: payload.title,
|
|
56
|
+
description: payload.description,
|
|
57
|
+
canonicalUrl: payload.canonicalUrl,
|
|
58
|
+
mediaType: payload.mediaType,
|
|
59
|
+
imageUrl: payload.imageUrl,
|
|
60
|
+
siteName: props.siteName,
|
|
61
|
+
redirectUrlJson: JSON.stringify(payload.canonicalUrl),
|
|
62
|
+
});
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
const { message } = error;
|
|
66
|
+
return res.status(400).json({ message });
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
package/seo/type.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type OpenGrapType = 'website' | 'article' | 'product' | 'profile' | 'music.song' | 'music.album' | 'music.playlist' | 'music.radio_station' | 'video.movie' | 'video.episode' | 'video.tv_show' | 'video.other';
|
|
2
|
+
export type MovieSubTag = 'video.actor:role' | 'video:director' | 'video:writer' | 'video:duration' | 'video:release_date' | 'video:tag';
|
|
3
|
+
export type MusicSubTag = 'music:duration' | 'music:album' | 'music:album:disc' | 'music:album:track' | 'music:musician';
|
|
4
|
+
export type SeoProps = {
|
|
5
|
+
siteName: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
canonicalUrl: string;
|
|
10
|
+
mediaType?: OpenGrapType;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
package/seo/type.js
ADDED
package/README.md
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
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
|