@xrystal/dto 26.6.27-v12 → 26.6.27-v13

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 (49) hide show
  1. package/README.md +210 -569
  2. package/dist/adapters/elysia.cjs +56 -58
  3. package/dist/adapters/elysia.d.ts +1 -1
  4. package/dist/adapters/elysia.js +56 -58
  5. package/dist/adapters/express.cjs +56 -58
  6. package/dist/adapters/express.d.ts +1 -1
  7. package/dist/adapters/express.js +56 -58
  8. package/dist/adapters/index.d.ts +1 -7
  9. package/dist/adapters/node.cjs +56 -58
  10. package/dist/adapters/node.d.ts +1 -1
  11. package/dist/adapters/node.js +56 -58
  12. package/dist/adapters/shared.d.ts +1 -1
  13. package/dist/adapters/socketio.cjs +56 -58
  14. package/dist/adapters/socketio.d.ts +1 -1
  15. package/dist/adapters/socketio.js +56 -58
  16. package/dist/adapters/websocket.cjs +56 -58
  17. package/dist/adapters/websocket.d.ts +1 -1
  18. package/dist/adapters/websocket.js +56 -58
  19. package/dist/dto/index.cjs +511 -57
  20. package/dist/dto/index.d.ts +2 -2
  21. package/dist/dto/index.js +504 -58
  22. package/dist/dto/response/assemble.d.ts +9 -1
  23. package/dist/{core → dto/response}/builder.d.ts +7 -7
  24. package/dist/{core → dto/response}/config.d.ts +4 -4
  25. package/dist/dto/response/index.cjs +466 -1
  26. package/dist/dto/response/index.d.ts +5 -0
  27. package/dist/dto/response/index.js +462 -2
  28. package/dist/{core → dto/response}/plugins.d.ts +2 -2
  29. package/dist/{core → dto/response}/presets.d.ts +3 -5
  30. package/dist/{core → dto/runtime}/context.d.ts +1 -1
  31. package/dist/{core → dto/runtime}/hooks.d.ts +1 -1
  32. package/dist/dto/runtime/{dto.cjs → index.cjs} +79 -47
  33. package/dist/dto/runtime/index.d.ts +4 -0
  34. package/dist/dto/runtime/{dto.js → index.js} +77 -48
  35. package/dist/{core → dto/runtime}/metadata.d.ts +1 -1
  36. package/dist/index.cjs +545 -547
  37. package/dist/index.d.ts +6 -3
  38. package/dist/index.js +545 -547
  39. package/package.json +54 -59
  40. package/dist/core/assemble-log.d.ts +0 -1
  41. package/dist/core/assemble-queue.d.ts +0 -1
  42. package/dist/core/assemble.d.ts +0 -9
  43. package/dist/core/index.cjs +0 -1002
  44. package/dist/core/index.d.ts +0 -14
  45. package/dist/core/index.js +0 -986
  46. package/dist/core/log-dto.d.ts +0 -1
  47. package/dist/core/queue-dto.d.ts +0 -1
  48. package/dist/core/response-dto.d.ts +0 -1
  49. package/dist/core/schema-utils.d.ts +0 -1
package/README.md CHANGED
@@ -1,333 +1,183 @@
1
1
  # @xrystal/dto
2
2
 
3
- Framework-agnostic **Data Transfer Object** library for **Express**, **Node/Bun HTTP**, **Elysia**, **WebSocket**, and **Socket.IO**.
3
+ Framework-agnostic **Data Transfer Object** library for Node.js, Bun, and TypeScript — HTTP, WebSocket, and Socket.IO.
4
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.
5
+ One predictable JSON contract. Three clear entry points.
6
6
 
7
7
  ---
8
8
 
9
- ## Why use it?
9
+ ## Getting started (5 minutes)
10
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
11
+ ### Install
17
12
 
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
- }
13
+ ```bash
14
+ npm install @xrystal/dto
15
+ # optional peers
16
+ npm install express elysia
34
17
  ```
35
18
 
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
19
+ ### Three concepts
50
20
 
51
- Everything hangs off one root: **`DTO`**.
21
+ | # | What | When |
22
+ |---|------|------|
23
+ | 1 | **`new DTO()`** | App setup — middleware, plugins, custom codes |
24
+ | 2 | **`res.builder`** / **`ctx.builder`** | Inside a handler — send API response to client |
25
+ | 3 | **`DTO.Response` / `DTO.Log` / `DTO.Queue`** | No request context — cron, logger, queue, tests |
52
26
 
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 |
27
+ **Rule of thumb:** client cevabı → `builder`. Düz JSON üretmek → static `DTO.*`.
60
28
 
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.
29
+ ### 1. Wire the runtime (once)
78
30
 
79
31
  ```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:
32
+ import express from 'express'
33
+ import { DTO } from '@xrystal/dto'
101
34
 
102
- ```typescript
103
- const bodyOnly = res.builder.dto.build() // same JSON shape, no dispatch yet
35
+ const runtime = new DTO({ withDefaultHeaders: true })
36
+ const app = express()
37
+ app.use(runtime.express())
104
38
  ```
105
39
 
106
- ### You **do not** have a request (cron, worker, unit test, logger, queue publisher)
40
+ This attaches `req.ctx` and `res.builder` on every request.
107
41
 
108
- No `req`, no `res`, no `ctx` — nothing to attach a builder to. Use **static shape DTOs**:
42
+ ### 2. Respond in a handler
109
43
 
110
44
  ```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()
45
+ app.get('/users/:id', (req, res) => {
46
+ return res.builder.as.ok(
47
+ { id: req.params.id },
48
+ 'User fetched',
49
+ { meta: { requestId: req.ctx.requestId } },
50
+ )
51
+ })
131
52
  ```
132
53
 
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 |
54
+ Sent JSON (fixed order: `status → message → payload? → code`):
141
55
 
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()
56
+ ```json
57
+ {
58
+ "status": true,
59
+ "message": "User fetched",
60
+ "payload": { "data": { "id": "42" }, "meta": { "requestId": "…" } },
61
+ "code": 0
62
+ }
163
63
  ```
164
64
 
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.
65
+ Presets (`.as.ok`, `.as.notFound`, …) dispatch automatically — **no `.build()`**.
166
66
 
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`.
67
+ ### 3. Static shape DTOs (no request)
174
68
 
175
69
  ```typescript
176
70
  import { DTO, NodeEnvEnum } from '@xrystal/dto'
177
71
 
72
+ // Log line
178
73
  DTO.Log.level('info')
179
- .service('users-api')
74
+ .service('api')
180
75
  .env(NodeEnvEnum.PROD)
181
- .message('User fetched')
182
- .payload({ userId: 1 })
76
+ .message('Server ready')
183
77
  .build()
184
78
 
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')
79
+ // Queue message
80
+ DTO.Queue.source('api')
207
81
  .eventType('user.created')
208
- .userId('u-1')
209
82
  .payload({ id: 1 })
210
83
  .build()
211
84
 
212
- DTO.Queue.schema({
213
- metadata: { source: 'billing', eventType: 'invoice.paid' },
214
- payload: { invoiceId: 9 },
215
- }).build()
85
+ // Response JSON only (no HTTP)
86
+ DTO.Response.status(true).message('OK').data({ id: 1 }).build()
216
87
  ```
217
88
 
218
- ---
89
+ Every shape DTO shares the same chain rules:
219
90
 
220
- ## Install
91
+ - Fixed field order
92
+ - `.schema({ … })` for partial input — **last call wins**
93
+ - `payload` in `.schema()` **replaces** the entire payload
221
94
 
222
- ```bash
223
- npm install @xrystal/dto
224
- ```
95
+ ### Elysia — same idea
225
96
 
226
- Peer dependencies (install only what you use):
97
+ ```typescript
98
+ import { Elysia } from 'elysia'
99
+ import { DTO } from '@xrystal/dto'
227
100
 
228
- ```bash
229
- npm install express # Express adapter
230
- npm install elysia # Elysia adapter
101
+ const runtime = new DTO()
102
+ new Elysia()
103
+ .use(runtime.elysia())
104
+ .get('/users/:id', ({ builder, requestId, params }) =>
105
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }),
106
+ )
231
107
  ```
232
108
 
233
- Subpath imports are available if you want a smaller surface:
109
+ ### When to use what
234
110
 
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
- ```
111
+ | Situation | Use |
112
+ |-----------|-----|
113
+ | Express / Node / Elysia handler | `res.builder` / `ctx.builder` |
114
+ | WebSocket / Socket.IO | `ws.builder` / `socket.builder` |
115
+ | Cron, worker, logger, queue publish | `DTO.Log`, `DTO.Queue`, `DTO.Response` |
116
+ | Custom HTTP status + headers | `res.builder.httpStatus(429).header(...).schema({...}).build()` |
117
+
118
+ Plugins and custom `codes` on `new DTO({ … })` apply **only to `builder`** — not to static `DTO.Log` / `DTO.Queue`.
243
119
 
244
120
  ---
245
121
 
246
- ## Quick start (Express)
122
+ ## Architecture
247
123
 
248
- ```typescript
249
- import express from 'express'
250
- import { DTO } from '@xrystal/dto'
124
+ Symmetric modules under `src/dto/`:
251
125
 
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
126
  ```
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
- }
127
+ dto/
128
+ ├── runtime/ ← new DTO(), middleware, ContextFactory, Hooks
129
+ │ ├── dto.ts
130
+ │ ├── context.ts
131
+ │ ├── metadata.ts
132
+ │ └── hooks.ts
133
+ ├── response/ ← ResponseDTO, Builder, presets, plugins
134
+ ├── log/
135
+ ├── queue/
136
+ └── shared/
137
+ ├── base-dto.ts
138
+ └── schema-utils.ts
269
139
  ```
270
140
 
271
- Middleware attaches:
141
+ ### Package exports
272
142
 
273
- - `req.ctx` — `{ requestId, startTime }`
274
- - `res.builder` — fluent response builder
143
+ | Import | Contents |
144
+ |--------|----------|
145
+ | `@xrystal/dto` | Everything |
146
+ | `@xrystal/dto/dto` | `DTO`, `createDTO`, runtime helpers |
147
+ | `@xrystal/dto/response` | `ResponseDTO`, `Builder`, presets |
148
+ | `@xrystal/dto/log` | `LogDTO` |
149
+ | `@xrystal/dto/queue` | `QueueDTO` |
150
+ | `@xrystal/dto/express` | Express middleware |
275
151
 
276
152
  ---
277
153
 
278
- ## Two ways to respond
279
-
280
- ### 1. Presets (recommended for most handlers)
281
-
282
- Call `.as.*` — dispatches automatically. **Do not call `.build()`.**
154
+ ## Response schema
283
155
 
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
- ```
156
+ | Field | Meaning |
157
+ |-------|---------|
158
+ | `status` | Business success/failure (`boolean`) — independent from HTTP status |
159
+ | `message` | Human-readable summary |
160
+ | `payload` | Optional wrapper (`data`, `meta`, pagination, …) |
161
+ | `code` | App code — default `0` SUCCESS, `1` CLIENT, `2` SERVER, `3` VALIDATION |
305
162
 
306
- **Preset argument shape**
163
+ Transport (HTTP status, headers, cookies) lives on **`builder`**, not on `DTO.Response`.
307
164
 
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()` |
165
+ ---
313
166
 
314
- ### 2. Manual chain (headers, cookies, custom cases)
167
+ ## Handler API
315
168
 
316
- Use when you need transport control or a schema-shaped body. Always end with `.build()`.
169
+ ### Presets (recommended)
317
170
 
318
171
  ```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()
172
+ res.builder.as.ok(data, 'Done')
173
+ res.builder.as.fail('Not found')
174
+ res.builder.as.notFound('User missing')
175
+ res.builder.as.tooManyRequests('Slow down')
328
176
  ```
329
177
 
330
- Or apply the body in one step with `.schema()`:
178
+ ### Manual chain
179
+
180
+ End with `.build()` when you need headers, cookies, or fine control:
331
181
 
332
182
  ```typescript
333
183
  return res.builder
@@ -337,324 +187,109 @@ return res.builder
337
187
  status: false,
338
188
  message: 'Slow down',
339
189
  code: 4,
340
- payload: {
341
- data: { retryAfter: 60 },
342
- meta: { requestId: req.ctx.requestId },
343
- },
190
+ payload: { data: { retryAfter: 60 } },
344
191
  })
345
192
  .build()
346
193
  ```
347
194
 
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 |
195
+ | Chain rule | Detail |
196
+ |------------|--------|
197
+ | Last call wins | Later `.schema()` or `.status()` overrides earlier values |
198
+ | `.schema()` is body-only | HTTP status and headers stay on the chain |
199
+ | `payload` in `.schema()` | Replaces entire payload — no merge |
369
200
 
370
201
  ---
371
202
 
372
- ## DTO setup
203
+ ## Shape DTOs
373
204
 
374
- Single entry point: **`DTO`**. Instance = runtime (middleware). Static namespaces = shape DTOs (`DTO.Response`, `DTO.Log`, `DTO.Queue`).
205
+ ### Response (`DTO.Response`)
375
206
 
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 |
207
+ Body only — `status → message → payload? → code`.
463
208
 
464
209
  ```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
- })
210
+ DTO.Response.status(true).message('OK').data({ id: 1 }).build()
474
211
 
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
- })
212
+ DTO.Response.schema({
213
+ status: false,
214
+ message: 'Not found',
215
+ code: 1,
216
+ }).build()
495
217
  ```
496
218
 
497
- ### Node HTTP
219
+ Inside a handler, `res.builder.dto` exposes the same `ResponseDTO` instance.
498
220
 
499
- ```typescript
500
- import { createServer } from 'node:http'
501
- import { DTO } from '@xrystal/dto'
221
+ ### Log (`DTO.Log`)
502
222
 
503
- const dto = new DTO()
223
+ Order: `level → service → message? → payload? → code? → timestamp → id? → env`
504
224
 
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:
225
+ Required: `level`, `service`, `env`.
516
226
 
517
227
  ```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)
228
+ DTO.Log.level('info')
229
+ .service('users-api')
230
+ .env(NodeEnvEnum.PROD)
231
+ .message('User fetched')
232
+ .build()
535
233
  ```
536
234
 
537
- `req.ctx` holds `requestId`; `res.builder` is attached by middleware (use `?.` until the handler runs).
235
+ ### Queue (`DTO.Queue`)
538
236
 
539
- ### Elysia
237
+ Order: `source → eventType → timestamp → correlationId → userId? → extraData?`
540
238
 
541
- The handler argument **is** the context. The plugin adds `builder`, `requestId`, and `startTime` via scoped `derive`:
239
+ Required: `source`, `eventType`. Timestamp and correlationId auto-generate.
542
240
 
543
241
  ```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
- )
242
+ DTO.Queue.source('billing')
243
+ .eventType('invoice.paid')
244
+ .payload({ invoiceId: 9 })
245
+ .build()
557
246
  ```
558
247
 
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
- ```
248
+ ---
570
249
 
571
- ### WebSocket
250
+ ## Runtime config
572
251
 
573
252
  ```typescript
574
- import { DTO } from '@xrystal/dto'
253
+ import { DTO, definePlugin } from '@xrystal/dto'
575
254
 
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
- },
255
+ const audit = definePlugin({
256
+ name: 'audit',
257
+ afterDispatch(result, ctx) {
258
+ console.log(ctx.context?.requestId, result.statusCode)
587
259
  },
588
260
  })
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
261
 
611
262
  const runtime = new DTO({
612
263
  withDefaultHeaders: true,
613
264
  codes: { RATE_LIMITED: 4 },
614
- })
265
+ plugins: [audit],
266
+ socket: { event: 'api:response' },
267
+ }).use(otherPlugin) // immutable — returns new DTO
615
268
 
616
- const app = express()
617
269
  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)
270
+ io.use(runtime.socketIo())
651
271
  ```
652
272
 
273
+ | Option | Affects |
274
+ |--------|---------|
275
+ | `plugins` | `builder` pipeline only |
276
+ | `codes` | `builder` presets and `.code()` |
277
+ | `withDefaultHeaders` | HTTP middleware |
278
+ | `socket.event` | WebSocket / Socket.IO emit name |
279
+
653
280
  ---
654
281
 
655
- ## Errors
282
+ ## Runtimes
656
283
 
657
- Map thrown errors to the standard body:
284
+ ```typescript
285
+ runtime.express()
286
+ runtime.node()
287
+ runtime.elysia()
288
+ runtime.websocket() / runtime.attachWebSocket(ws)
289
+ runtime.socketIo() / runtime.attachSocketIo(socket)
290
+ ```
291
+
292
+ ### Errors in handlers
658
293
 
659
294
  ```typescript
660
295
  try {
@@ -665,60 +300,66 @@ try {
665
300
  }
666
301
  ```
667
302
 
668
- Built-in error types: `ClientError`, `ServerError`, `ValidationError`. Unknown errors are masked when `verbose: false`.
303
+ Built-in: `ClientError`, `ServerError`, `ValidationError`.
669
304
 
670
305
  ---
671
306
 
672
- ## Custom codes & HTTP status
307
+ ## Full-stack sketch
673
308
 
674
309
  ```typescript
675
- const dto = new DTO({ codes: { RATE_LIMITED: 4 } })
310
+ import express from 'express'
311
+ import { DTO, NodeEnvEnum } from '@xrystal/dto'
676
312
 
677
- return res.builder
678
- .code(4)
679
- .httpStatus(429)
680
- .status(false)
681
- .message('Too many requests')
682
- .build()
313
+ const runtime = new DTO({ withDefaultHeaders: true })
314
+ const app = express()
315
+ app.use(runtime.express())
683
316
 
684
- // Or when HTTP maps cleanly:
685
- return res.builder.as.tooManyRequests('Too many requests')
317
+ app.get('/users/:id', async (req, res) => {
318
+ const user = await db.findUser(req.params.id)
319
+ if (!user) return res.builder.as.notFound('User not found')
320
+
321
+ console.log(
322
+ DTO.Log.level('info')
323
+ .service('users-api')
324
+ .env(NodeEnvEnum.PROD)
325
+ .payload({ userId: user.id })
326
+ .build(),
327
+ )
328
+
329
+ return res.builder.as.ok(user, 'User fetched', {
330
+ meta: { requestId: req.ctx.requestId },
331
+ })
332
+ })
686
333
  ```
687
334
 
688
335
  ---
689
336
 
690
- ## Release (maintainers)
691
-
692
- **Source of truth:** `package.json` `"version"`. Git tag must be **identical** — no env override.
337
+ ## HTTP presets reference
693
338
 
694
- Format: `YY.MM.DD-vN` (e.g. `26.06.27-v8`).
695
-
696
- ### Quick tag (commit already done)
339
+ | Preset | HTTP | Code |
340
+ |--------|------|------|
341
+ | `.ok()` | 200 | SUCCESS |
342
+ | `.created()` | 201 | SUCCESS |
343
+ | `.fail()` | 400 | CLIENT |
344
+ | `.notFound()` | 404 | CLIENT |
345
+ | `.unprocessable()` | 422 | VALIDATION |
346
+ | `.tooManyRequests()` | 429 | CLIENT |
347
+ | `.error()` | 500 | SERVER |
697
348
 
698
- Bump `package.json`, commit, then:
349
+ See source `dto/response/presets.ts` for the full list.
699
350
 
700
- ```bash
701
- npm run tag
702
- ```
351
+ ---
703
352
 
704
- Reads version from **committed** `package.json`, tags **current commit**, pushes branch + tag → Publish workflow runs.
353
+ ## Release (maintainers)
705
354
 
706
- ### Full release (lint + test + build + commit + tag)
355
+ Version lives in `package.json` (`YY.MM.DD-vN`). Git tag must match exactly.
707
356
 
708
357
  ```bash
709
- # 1. Bump version in package.json
710
- # 2. Run everything:
711
- npm run release
358
+ npm run release # lint → test → build → commit → tag → push
359
+ npm run tag # tag only (after manual commit)
360
+ npm run version:check
712
361
  ```
713
362
 
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
363
  ---
723
364
 
724
365
  MIT