@xrystal/dto 26.6.27-v12

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 (87) hide show
  1. package/README.md +724 -0
  2. package/dist/adapters/elysia.cjs +820 -0
  3. package/dist/adapters/elysia.d.ts +138 -0
  4. package/dist/adapters/elysia.js +816 -0
  5. package/dist/adapters/express.cjs +789 -0
  6. package/dist/adapters/express.d.ts +49 -0
  7. package/dist/adapters/express.js +785 -0
  8. package/dist/adapters/index.d.ts +7 -0
  9. package/dist/adapters/node.cjs +790 -0
  10. package/dist/adapters/node.d.ts +27 -0
  11. package/dist/adapters/node.js +786 -0
  12. package/dist/adapters/shared.d.ts +11 -0
  13. package/dist/adapters/socketio.cjs +767 -0
  14. package/dist/adapters/socketio.d.ts +31 -0
  15. package/dist/adapters/socketio.js +763 -0
  16. package/dist/adapters/websocket.cjs +759 -0
  17. package/dist/adapters/websocket.d.ts +31 -0
  18. package/dist/adapters/websocket.js +754 -0
  19. package/dist/core/assemble-log.d.ts +1 -0
  20. package/dist/core/assemble-queue.d.ts +1 -0
  21. package/dist/core/assemble.d.ts +9 -0
  22. package/dist/core/builder.d.ts +49 -0
  23. package/dist/core/config.d.ts +10 -0
  24. package/dist/core/context.d.ts +5 -0
  25. package/dist/core/hooks.d.ts +8 -0
  26. package/dist/core/index.cjs +1002 -0
  27. package/dist/core/index.d.ts +14 -0
  28. package/dist/core/index.js +986 -0
  29. package/dist/core/log-dto.d.ts +1 -0
  30. package/dist/core/metadata.d.ts +6 -0
  31. package/dist/core/plugins.d.ts +10 -0
  32. package/dist/core/presets.d.ts +32 -0
  33. package/dist/core/queue-dto.d.ts +1 -0
  34. package/dist/core/response-dto.d.ts +1 -0
  35. package/dist/core/schema-utils.d.ts +1 -0
  36. package/dist/dto/index.cjs +909 -0
  37. package/dist/dto/index.d.ts +6 -0
  38. package/dist/dto/index.js +898 -0
  39. package/dist/dto/log/assemble.d.ts +3 -0
  40. package/dist/dto/log/builder.d.ts +41 -0
  41. package/dist/dto/log/index.cjs +152 -0
  42. package/dist/dto/log/index.d.ts +3 -0
  43. package/dist/dto/log/index.js +149 -0
  44. package/dist/dto/log/types.d.ts +26 -0
  45. package/dist/dto/queue/assemble.d.ts +2 -0
  46. package/dist/dto/queue/builder.d.ts +35 -0
  47. package/dist/dto/queue/index.cjs +154 -0
  48. package/dist/dto/queue/index.d.ts +3 -0
  49. package/dist/dto/queue/index.js +151 -0
  50. package/dist/dto/queue/types.d.ts +34 -0
  51. package/dist/dto/response/assemble.d.ts +1 -0
  52. package/dist/dto/response/dto.d.ts +39 -0
  53. package/dist/dto/response/index.cjs +179 -0
  54. package/dist/dto/response/index.d.ts +2 -0
  55. package/dist/dto/response/index.js +176 -0
  56. package/dist/dto/runtime/dto.cjs +1318 -0
  57. package/dist/dto/runtime/dto.d.ts +64 -0
  58. package/dist/dto/runtime/dto.js +1315 -0
  59. package/dist/dto/shared/base-dto.d.ts +12 -0
  60. package/dist/dto/shared/schema-utils.d.ts +1 -0
  61. package/dist/enums/node-env.d.ts +5 -0
  62. package/dist/errors/codes.d.ts +15 -0
  63. package/dist/errors/error.d.ts +32 -0
  64. package/dist/errors/index.d.ts +2 -0
  65. package/dist/index.cjs +1482 -0
  66. package/dist/index.d.ts +22 -0
  67. package/dist/index.js +1433 -0
  68. package/dist/types/builder.d.ts +15 -0
  69. package/dist/types/codes.d.ts +7 -0
  70. package/dist/types/context.d.ts +16 -0
  71. package/dist/types/hooks.d.ts +7 -0
  72. package/dist/types/index.d.ts +10 -0
  73. package/dist/types/log.d.ts +1 -0
  74. package/dist/types/metadata.d.ts +7 -0
  75. package/dist/types/options.d.ts +24 -0
  76. package/dist/types/plugin.d.ts +35 -0
  77. package/dist/types/queue.d.ts +1 -0
  78. package/dist/types/response.d.ts +43 -0
  79. package/dist/utils/constants/http.d.ts +2 -0
  80. package/dist/utils/constants/index.d.ts +3 -0
  81. package/dist/utils/constants/security-headers.d.ts +1 -0
  82. package/dist/utils/constants/status-codes.d.ts +35 -0
  83. package/dist/utils/helpers/index.d.ts +2 -0
  84. package/dist/utils/helpers/request-id.d.ts +3 -0
  85. package/dist/utils/helpers/uuid.d.ts +2 -0
  86. package/dist/utils/index.d.ts +2 -0
  87. package/package.json +130 -0
package/README.md ADDED
@@ -0,0 +1,724 @@
1
+ # @xrystal/dto
2
+
3
+ Framework-agnostic **Data Transfer Object** library for **Express**, **Node/Bun HTTP**, **Elysia**, **WebSocket**, and **Socket.IO**.
4
+
5
+ Every runtime shares the same response builder, and the same modular DTO pipeline — response, log, and queue shapes with predictable JSON field order.
6
+
7
+ ---
8
+
9
+ ## Why use it?
10
+
11
+ - **One response schema** across HTTP and realtime transports
12
+ - **Presets** for common cases (`.as.ok`, `.as.notFound`, …) — no boilerplate
13
+ - **Manual chain** when you need headers, cookies, rate limits, or a full body object via `.schema()`
14
+ - **Plugins** to audit, trace, or transform responses middleware-style
15
+ - **Modular DTOs** — response (request-scoped), log and queue (static global builders)
16
+ - **TypeScript-first** with strict field ordering in every JSON body
17
+
18
+ ---
19
+
20
+ ## Response schema
21
+
22
+ Every response body follows this fixed field order:
23
+
24
+ ```json
25
+ {
26
+ "status": true,
27
+ "message": "User fetched",
28
+ "payload": {
29
+ "data": { "id": 1 },
30
+ "meta": { "requestId": "abc-123" }
31
+ },
32
+ "code": 0
33
+ }
34
+ ```
35
+
36
+ | Field | Type | Required in JSON | Meaning |
37
+ |-------|------|------------------|---------|
38
+ | `status` | `boolean` | always | Business outcome — `true` success, `false` failure. Independent from HTTP status. |
39
+ | `message` | `string` | always | Human-readable summary for clients and logs. |
40
+ | `payload` | `object` | optional | Wrapper for extra data. Omitted when empty. |
41
+ | `payload.data` | `any` | optional | Primary result — set via `.data()` or preset first argument. |
42
+ | `payload.meta` | `object` | optional | Tracing, pagination context, etc. |
43
+ | `code` | `number` | always | App-level code. Defaults: `0` SUCCESS, `1` CLIENT, `2` SERVER, `3` VALIDATION. Override or extend via `DTO` config. |
44
+
45
+ Transport layer (HTTP status, headers, cookies) is **separate** from the body schema.
46
+
47
+ ---
48
+
49
+ ## Mental model
50
+
51
+ Everything hangs off one root: **`DTO`**.
52
+
53
+ | Layer | Entry | Scope | Use for |
54
+ |-------|--------|-------|---------|
55
+ | Runtime | `new DTO()` | App-wide config | Middleware, plugins, codes, socket event |
56
+ | Response body | `DTO.Response` or `res.builder.dto` | Static or per-request | JSON body shape only |
57
+ | Response dispatch | `res.builder` / `ctx.builder` | Per request | Body + HTTP status + headers + plugins |
58
+ | Log | `DTO.Log` / `LogDTO` | Static | Structured logs |
59
+ | Queue | `DTO.Queue` / `QueueDTO` | Static | Message bus payloads |
60
+
61
+ All shape DTOs extend the same rules:
62
+
63
+ - Fluent chain in **fixed field order**
64
+ - `.schema()` for partial input — **last call wins**
65
+ - `payload` in `.schema()` **replaces** the entire payload (no merge)
66
+
67
+ `Builder` is transport on top of `ResponseDTO` — same split as HTTP status vs JSON body.
68
+
69
+ ---
70
+
71
+ ## When to use what
72
+
73
+ This is the most important distinction in the library.
74
+
75
+ ### You **have** a request (Express, Node, Elysia, WebSocket, Socket.IO)
76
+
77
+ Middleware from `new DTO()` attaches **`builder`** to the request/socket. Use that in handlers — it has context, dispatch, HTTP status, headers, and plugins.
78
+
79
+ ```typescript
80
+ // Express / Node — res.builder
81
+ app.get('/users/:id', (req, res) => {
82
+ return res.builder.as.ok({ id: req.params.id }, 'User fetched', {
83
+ meta: { requestId: req.ctx.requestId },
84
+ })
85
+ })
86
+
87
+ // Elysia — ctx.builder (same idea, flat context)
88
+ .get('/users/:id', ({ builder, requestId, params }) =>
89
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }),
90
+ )
91
+
92
+ // Manual chain when you need HTTP + body control
93
+ return res.builder
94
+ .httpStatus(429)
95
+ .header('Retry-After', '60')
96
+ .schema({ status: false, message: 'Slow down', code: 4 })
97
+ .build()
98
+ ```
99
+
100
+ `DTO.Response` is **not** used here directly — `res.builder` already owns an internal `ResponseDTO`. You can inspect or reuse it:
101
+
102
+ ```typescript
103
+ const bodyOnly = res.builder.dto.build() // same JSON shape, no dispatch yet
104
+ ```
105
+
106
+ ### You **do not** have a request (cron, worker, unit test, logger, queue publisher)
107
+
108
+ No `req`, no `res`, no `ctx` — nothing to attach a builder to. Use **static shape DTOs**:
109
+
110
+ ```typescript
111
+ // Log line for your logger transport
112
+ const logLine = DTO.Log.level('info')
113
+ .service('billing-cron')
114
+ .env(NodeEnvEnum.PROD)
115
+ .message('Invoices synced')
116
+ .build()
117
+ logger.info(logLine)
118
+
119
+ // Rabbit / SQS / Bull payload
120
+ const message = DTO.Queue.source('billing-cron')
121
+ .eventType('invoice.synced')
122
+ .payload({ count: 42 })
123
+ .build()
124
+ await queue.publish(message)
125
+
126
+ // Plain response JSON (e.g. cache, test fixture, RPC payload without HTTP)
127
+ const rpcBody = DTO.Response.status(true)
128
+ .message('Computed')
129
+ .data({ total: 100 })
130
+ .build()
131
+ ```
132
+
133
+ ### Summary
134
+
135
+ | Situation | Use | Returns |
136
+ |-----------|-----|---------|
137
+ | HTTP handler (Express/Node/Elysia) | `res.builder` / `ctx.builder` | Dispatches response to client |
138
+ | WebSocket / Socket.IO handler | `ws.builder` / `socket.builder` | Sends JSON on wire |
139
+ | Cron, worker, logger, queue | `DTO.Log`, `DTO.Queue`, `DTO.Response` | Plain object — you send/write it |
140
+ | Need body JSON only inside a handler | `res.builder.dto` | `ResponseBody` object |
141
+
142
+ ---
143
+
144
+ ## Response DTO
145
+
146
+ Body only (`status → message → payload? → code`). No HTTP status here.
147
+
148
+ ```typescript
149
+ import { DTO } from '@xrystal/dto'
150
+
151
+ DTO.Response.status(true)
152
+ .message('User fetched')
153
+ .data({ id: 1 })
154
+ .build()
155
+
156
+ // Partial object — same .schema() rules as the builder chain
157
+ DTO.Response.schema({
158
+ status: false,
159
+ message: 'Not found',
160
+ code: 1,
161
+ payload: { data: { reason: 'missing' } },
162
+ }).build()
163
+ ```
164
+
165
+ In a handler, `res.builder` delegates body fields to an internal `ResponseDTO`. Access it via `res.builder.dto` when you only need the shape.
166
+
167
+ ---
168
+
169
+ ## Log DTO
170
+
171
+ Fixed field order: `level → service → message? → payload? → code? → timestamp → id? → env`
172
+
173
+ **Required at build:** `level`, `service`, `env`.
174
+
175
+ ```typescript
176
+ import { DTO, NodeEnvEnum } from '@xrystal/dto'
177
+
178
+ DTO.Log.level('info')
179
+ .service('users-api')
180
+ .env(NodeEnvEnum.PROD)
181
+ .message('User fetched')
182
+ .payload({ userId: 1 })
183
+ .build()
184
+
185
+ // One-shot via .schema()
186
+ DTO.Log.schema({
187
+ level: 'error',
188
+ service: 'auth',
189
+ env: NodeEnvEnum.DEV,
190
+ message: 'Login failed',
191
+ code: 'AUTH_FAIL',
192
+ }).build()
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Queue DTO
198
+
199
+ Shape: `{ metadata, payload }` — metadata order `source → eventType → timestamp → correlationId → userId? → extraData?`
200
+
201
+ **Required at build:** `source`, `eventType`.
202
+
203
+ ```typescript
204
+ import { DTO } from '@xrystal/dto'
205
+
206
+ DTO.Queue.source('users-api')
207
+ .eventType('user.created')
208
+ .userId('u-1')
209
+ .payload({ id: 1 })
210
+ .build()
211
+
212
+ DTO.Queue.schema({
213
+ metadata: { source: 'billing', eventType: 'invoice.paid' },
214
+ payload: { invoiceId: 9 },
215
+ }).build()
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Install
221
+
222
+ ```bash
223
+ npm install @xrystal/dto
224
+ ```
225
+
226
+ Peer dependencies (install only what you use):
227
+
228
+ ```bash
229
+ npm install express # Express adapter
230
+ npm install elysia # Elysia adapter
231
+ ```
232
+
233
+ Subpath imports are available if you want a smaller surface:
234
+
235
+ ```typescript
236
+ import { DTO, LogDTO, ResponseDTO, QueueDTO, Builder } from '@xrystal/dto'
237
+ import { DTO } from '@xrystal/dto/dto' // runtime only
238
+ import { ResponseDTO } from '@xrystal/dto/response'
239
+ import { LogDTO } from '@xrystal/dto/log'
240
+ import { QueueDTO } from '@xrystal/dto/queue'
241
+ import { expressMiddleware } from '@xrystal/dto/express'
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Quick start (Express)
247
+
248
+ ```typescript
249
+ import express from 'express'
250
+ import { DTO } from '@xrystal/dto'
251
+
252
+ const app = express()
253
+ app.use(new DTO().express())
254
+
255
+ app.get('/users/:id', (req, res) => {
256
+ return res.builder.as.ok({ id: req.params.id }, 'User fetched')
257
+ })
258
+ ```
259
+
260
+ Response sent to the client:
261
+
262
+ ```json
263
+ {
264
+ "status": true,
265
+ "message": "User fetched",
266
+ "payload": { "data": { "id": "42" } },
267
+ "code": 0
268
+ }
269
+ ```
270
+
271
+ Middleware attaches:
272
+
273
+ - `req.ctx` — `{ requestId, startTime }`
274
+ - `res.builder` — fluent response builder
275
+
276
+ ---
277
+
278
+ ## Two ways to respond
279
+
280
+ ### 1. Presets (recommended for most handlers)
281
+
282
+ Call `.as.*` — dispatches automatically. **Do not call `.build()`.**
283
+
284
+ ```typescript
285
+ // Success
286
+ return res.builder.as.ok({ id: 1 }, 'User fetched')
287
+
288
+ // With payload extras (meta, pagination fields, …)
289
+ return res.builder.as.ok(
290
+ { id: 1 },
291
+ 'User fetched',
292
+ { meta: { requestId: req.ctx.requestId } },
293
+ )
294
+
295
+ // Generic failures
296
+ return res.builder.as.fail('Could not fetch users')
297
+ return res.builder.as.fail({ reason: 'invalid_query' }, 'Could not fetch users')
298
+ return res.builder.as.error({ ref: 'db-01' }, 'Something went wrong')
299
+
300
+ // Exact HTTP status
301
+ return res.builder.as.notFound('User not found')
302
+ return res.builder.as.unauthorized('Token expired')
303
+ return res.builder.as.tooManyRequests('Slow down')
304
+ ```
305
+
306
+ **Preset argument shape**
307
+
308
+ | Pattern | Example |
309
+ |---------|---------|
310
+ | Message only | `.fail('Not found')` |
311
+ | Message + extras | `.fail('Rate limited', { meta: { retryAfter: 60 } })` |
312
+ | Data + message + extras | `.ok(data, 'Done', { meta: { … } })` — same as `.fail()` / `.error()` |
313
+
314
+ ### 2. Manual chain (headers, cookies, custom cases)
315
+
316
+ Use when you need transport control or a schema-shaped body. Always end with `.build()`.
317
+
318
+ ```typescript
319
+ return res.builder
320
+ .httpStatus(429)
321
+ .header('Retry-After', '60')
322
+ .status(false)
323
+ .code(4)
324
+ .message('Slow down')
325
+ .data({ retryAfter: 60 })
326
+ .payload({ meta: { requestId: req.ctx.requestId } })
327
+ .build()
328
+ ```
329
+
330
+ Or apply the body in one step with `.schema()`:
331
+
332
+ ```typescript
333
+ return res.builder
334
+ .httpStatus(429)
335
+ .header('Retry-After', '60')
336
+ .schema({
337
+ status: false,
338
+ message: 'Slow down',
339
+ code: 4,
340
+ payload: {
341
+ data: { retryAfter: 60 },
342
+ meta: { requestId: req.ctx.requestId },
343
+ },
344
+ })
345
+ .build()
346
+ ```
347
+
348
+ **Chain rules**
349
+
350
+ | Rule | Detail |
351
+ |------|--------|
352
+ | Last call wins | `.schema()` overrides earlier `.status()` / `.code()` / `.data()`; a later `.status()` overrides `.schema()` too. |
353
+ | `.schema()` body only | Put `meta`, `data`, etc. inside `payload`. Put `httpStatus`, headers, cookies on the chain. |
354
+ | `payload` in `.schema()` | Replaces the entire payload — no merge with earlier chain calls. |
355
+ | Required body fields | `status`, `message`, and `code` are always emitted. Defaults fill omissions at `.build()` time. |
356
+ | `.data()` vs `.payload()` | Separate methods — `.data(x)` sets `payload.data` only; `.payload({ meta })` sets other payload fields. |
357
+
358
+ | Method | Purpose |
359
+ |--------|---------|
360
+ | `.httpStatus(200)` | HTTP status code |
361
+ | `.schema({ … })` | Apply partial response body |
362
+ | `.status(true)` | Body `status` field |
363
+ | `.code(0)` | Body `code` field |
364
+ | `.message('…')` | Body `message` |
365
+ | `.data(x)` | Sets `payload.data` |
366
+ | `.payload({ meta })` | Other payload fields |
367
+ | `.header()` / `.setCookies()` | Response headers and cookies |
368
+ | `.build()` | Assemble and dispatch |
369
+
370
+ ---
371
+
372
+ ## DTO setup
373
+
374
+ Single entry point: **`DTO`**. Instance = runtime (middleware). Static namespaces = shape DTOs (`DTO.Response`, `DTO.Log`, `DTO.Queue`).
375
+
376
+ ```typescript
377
+ import { DTO, definePlugin } from '@xrystal/dto'
378
+
379
+ const requestIdPlugin = definePlugin({
380
+ name: 'request-id',
381
+ priority: -10,
382
+ afterBuild(result, ctx) {
383
+ return {
384
+ ...result,
385
+ headers: { ...result.headers, 'X-Request-Id': ctx.context?.requestId ?? '' },
386
+ }
387
+ },
388
+ })
389
+
390
+ const dto = new DTO({
391
+ withDefaultHeaders: true,
392
+ codes: { RATE_LIMITED: 4 },
393
+ plugins: [requestIdPlugin],
394
+ socket: { event: 'api:response' },
395
+ }).use(auditPlugin) // immutable — returns a new DTO
396
+
397
+ app.use(dto.express())
398
+ io.use(dto.socketIo())
399
+ ```
400
+
401
+ Default codes (always merged; override only what you need):
402
+
403
+ | Key | Default | Typical use |
404
+ |-----|---------|-------------|
405
+ | `SUCCESS` | `0` | Happy path |
406
+ | `CLIENT` | `1` | Bad input, auth, not found, … |
407
+ | `SERVER` | `2` | Internal errors |
408
+ | `VALIDATION` | `3` | Schema / field validation |
409
+
410
+ ---
411
+
412
+ ## Core presets
413
+
414
+ | Method | HTTP | Code | Purpose |
415
+ |--------|------|------|---------|
416
+ | `.ok(data?, message?, extras?)` | 200 | SUCCESS | Success |
417
+ | `.fail(data?, message?, extras?)` | 400 | CLIENT | Generic client failure |
418
+ | `.error(data?, message?, extras?)` | 500 | SERVER | Generic server failure |
419
+ | `.created(data?, message?, extras?)` | 201 | SUCCESS | Resource created |
420
+ | `.accepted(message?, extras?)` | 202 | SUCCESS | Accepted |
421
+ | `.noContent(extras?)` | 204 | SUCCESS | No body |
422
+
423
+ ---
424
+
425
+ ## HTTP-specific presets
426
+
427
+ Use when you need an exact HTTP status. Core `.fail()` / `.error()` cover most cases.
428
+
429
+ ### Client (4xx)
430
+
431
+ | Method | HTTP | Code |
432
+ |--------|------|------|
433
+ | `.badRequest(…)` | 400 | CLIENT |
434
+ | `.unauthorized(…)` | 401 | CLIENT |
435
+ | `.forbidden(…)` | 403 | CLIENT |
436
+ | `.notFound(…)` | 404 | CLIENT |
437
+ | `.conflict(…)` | 409 | CLIENT |
438
+ | `.unprocessable(…)` | 422 | VALIDATION |
439
+ | `.tooManyRequests(…)` | 429 | CLIENT |
440
+
441
+ ### Server (5xx)
442
+
443
+ | Method | HTTP | Code |
444
+ |--------|------|------|
445
+ | `.internalServerError(…)` | 500 | SERVER |
446
+ | `.serviceUnavailable(…)` | 503 | SERVER |
447
+ | `.gatewayTimeout(…)` | 504 | SERVER |
448
+
449
+ All HTTP presets share the same argument shape as `.ok()` / `.fail()`.
450
+
451
+ ---
452
+
453
+ ## Plugins
454
+
455
+ Middleware-style pipeline. Lower `priority` runs first (default `0`).
456
+
457
+ | Hook | When | Use for |
458
+ |------|------|---------|
459
+ | `beforeBuild` | Before body assembly | Mutate builder state |
460
+ | `afterBuild` | After `FinalResult` is ready | Transform body, headers, cookies |
461
+ | `beforeDispatch` | Same stage as `afterBuild` | Alias hook for dispatch pipeline |
462
+ | `afterDispatch` | After response is sent | Logging, metrics, audit |
463
+
464
+ ```typescript
465
+ import { definePlugin } from '@xrystal/dto'
466
+
467
+ export const auditPlugin = definePlugin({
468
+ name: 'audit',
469
+ priority: 0,
470
+ afterDispatch(result, ctx) {
471
+ console.log(ctx.context?.requestId, result.statusCode, result.body.code)
472
+ },
473
+ })
474
+
475
+ new DTO({ plugins: [auditPlugin] })
476
+ new DTO().use(auditPlugin, otherPlugin)
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Runtime guides
482
+
483
+ ### Express
484
+
485
+ ```typescript
486
+ import express from 'express'
487
+ import { DTO } from '@xrystal/dto'
488
+
489
+ const app = express()
490
+ app.use(new DTO().express())
491
+
492
+ app.get('/users/:id', (req, res) => {
493
+ return res.builder.as.ok({ id: req.params.id }, 'User fetched')
494
+ })
495
+ ```
496
+
497
+ ### Node HTTP
498
+
499
+ ```typescript
500
+ import { createServer } from 'node:http'
501
+ import { DTO } from '@xrystal/dto'
502
+
503
+ const dto = new DTO()
504
+
505
+ createServer((req, res) => {
506
+ dto.node()(req, res, () => {
507
+ if (req.url === '/health') return res.builder?.as.ok({ ok: true }, 'Up')
508
+ return res.builder?.as.fail('Not found')
509
+ })
510
+ }).listen(3000)
511
+ ```
512
+
513
+ ### Bun HTTP
514
+
515
+ Bun uses the Node adapter via `node:http` — same API:
516
+
517
+ ```typescript
518
+ import { createServer } from 'node:http'
519
+ import { DTO } from '@xrystal/dto'
520
+
521
+ const dto = new DTO()
522
+
523
+ createServer((req, res) => {
524
+ dto.node()(req, res, () => {
525
+ if (req.url === '/health') return res.builder?.as.ok({ ok: true }, 'Up')
526
+
527
+ return res.builder
528
+ ?.code(4)
529
+ .httpStatus(429)
530
+ .status(false)
531
+ .message('Too many requests')
532
+ .build()
533
+ })
534
+ }).listen(3000)
535
+ ```
536
+
537
+ `req.ctx` holds `requestId`; `res.builder` is attached by middleware (use `?.` until the handler runs).
538
+
539
+ ### Elysia
540
+
541
+ The handler argument **is** the context. The plugin adds `builder`, `requestId`, and `startTime` via scoped `derive`:
542
+
543
+ ```typescript
544
+ import { Elysia } from 'elysia'
545
+ import { DTO } from '@xrystal/dto'
546
+
547
+ const dto = new DTO()
548
+
549
+ new Elysia()
550
+ .use(dto.elysia())
551
+ .get('/health', ({ builder, requestId }) =>
552
+ builder.as.ok({ healthy: true }, 'OK', { meta: { requestId } }),
553
+ )
554
+ .get('/users/:id', ({ builder, requestId, params }) =>
555
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }),
556
+ )
557
+ ```
558
+
559
+ Outside a request (e.g. startup log):
560
+
561
+ ```typescript
562
+ console.log(
563
+ DTO.Log.level('info')
564
+ .service('api')
565
+ .env(NodeEnvEnum.PROD)
566
+ .message('Server listening')
567
+ .build(),
568
+ )
569
+ ```
570
+
571
+ ### WebSocket
572
+
573
+ ```typescript
574
+ import { DTO } from '@xrystal/dto'
575
+
576
+ const dto = new DTO()
577
+
578
+ Bun.serve({
579
+ websocket: {
580
+ open(ws) {
581
+ dto.attachWebSocket(ws)
582
+ ws.builder?.as.ok({ connected: true }, 'Ready')
583
+ },
584
+ message(ws, raw) {
585
+ ws.builder?.as.ok({ echo: raw.toString() }, 'Echo')
586
+ },
587
+ },
588
+ })
589
+ ```
590
+
591
+ ### Socket.IO
592
+
593
+ ```typescript
594
+ dto.attachSocketIo(socket)
595
+ socket.builder?.as.ok({ room: 'lobby' }, 'Connected')
596
+
597
+ io.use(dto.socketIo())
598
+ socket.builder?.as.ok({ socketId: socket.id }, 'Connected')
599
+ ```
600
+
601
+ WebSocket and Socket.IO have no HTTP status — use presets or `.build()`. JSON is sent via `ws.send` / `socket.emit`.
602
+
603
+ ---
604
+
605
+ ## Full-stack example
606
+
607
+ ```typescript
608
+ import express from 'express'
609
+ import { DTO, NodeEnvEnum } from '@xrystal/dto'
610
+
611
+ const runtime = new DTO({
612
+ withDefaultHeaders: true,
613
+ codes: { RATE_LIMITED: 4 },
614
+ })
615
+
616
+ const app = express()
617
+ app.use(runtime.express())
618
+
619
+ app.get('/users/:id', async (req, res) => {
620
+ try {
621
+ const user = await db.findUser(req.params.id)
622
+ if (!user) return res.builder.as.notFound('User not found')
623
+
624
+ // Side-effect: structured log (no req/res needed for shape)
625
+ console.log(
626
+ DTO.Log.level('info')
627
+ .service('users-api')
628
+ .env(NodeEnvEnum.PROD)
629
+ .message('User fetched')
630
+ .payload({ userId: user.id })
631
+ .build(),
632
+ )
633
+
634
+ return res.builder.as.ok(user, 'User fetched', {
635
+ meta: { requestId: req.ctx.requestId },
636
+ })
637
+ } catch (err) {
638
+ return res.builder.fromError(err).build()
639
+ }
640
+ })
641
+
642
+ // Cron job — no builder, static DTOs only
643
+ setInterval(() => {
644
+ const event = DTO.Queue.source('users-api')
645
+ .eventType('users.active.count')
646
+ .payload({ count: db.activeCount() })
647
+ .build()
648
+
649
+ queue.publish(event)
650
+ }, 60_000)
651
+ ```
652
+
653
+ ---
654
+
655
+ ## Errors
656
+
657
+ Map thrown errors to the standard body:
658
+
659
+ ```typescript
660
+ try {
661
+ const user = await fetchUser(id)
662
+ return res.builder.as.ok(user, 'User fetched')
663
+ } catch (err) {
664
+ return res.builder.fromError(err).build()
665
+ }
666
+ ```
667
+
668
+ Built-in error types: `ClientError`, `ServerError`, `ValidationError`. Unknown errors are masked when `verbose: false`.
669
+
670
+ ---
671
+
672
+ ## Custom codes & HTTP status
673
+
674
+ ```typescript
675
+ const dto = new DTO({ codes: { RATE_LIMITED: 4 } })
676
+
677
+ return res.builder
678
+ .code(4)
679
+ .httpStatus(429)
680
+ .status(false)
681
+ .message('Too many requests')
682
+ .build()
683
+
684
+ // Or when HTTP maps cleanly:
685
+ return res.builder.as.tooManyRequests('Too many requests')
686
+ ```
687
+
688
+ ---
689
+
690
+ ## Release (maintainers)
691
+
692
+ **Source of truth:** `package.json` `"version"`. Git tag must be **identical** — no env override.
693
+
694
+ Format: `YY.MM.DD-vN` (e.g. `26.06.27-v8`).
695
+
696
+ ### Quick tag (commit already done)
697
+
698
+ Bump `package.json`, commit, then:
699
+
700
+ ```bash
701
+ npm run tag
702
+ ```
703
+
704
+ Reads version from **committed** `package.json`, tags **current commit**, pushes branch + tag → Publish workflow runs.
705
+
706
+ ### Full release (lint + test + build + commit + tag)
707
+
708
+ ```bash
709
+ # 1. Bump version in package.json
710
+ # 2. Run everything:
711
+ npm run release
712
+ ```
713
+
714
+ | Command | What it does |
715
+ |---------|----------------|
716
+ | `npm run version:check` | Warns if HEAD tag ≠ package.json |
717
+ | `npm run tag` | Tag HEAD from package.json → push branch + tag |
718
+ | `npm run release` | lint → test → build → commit → `npm run tag` |
719
+
720
+ GitHub Publish verifies `tag === package.json version` before npm publish.
721
+
722
+ ---
723
+
724
+ MIT