@xrystal/dto 26.6.27-v12 → 26.6.27-v14

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 (81) hide show
  1. package/README.md +405 -529
  2. package/dist/adapters/elysia.cjs +245 -221
  3. package/dist/adapters/elysia.js +244 -222
  4. package/dist/adapters/express.cjs +231 -219
  5. package/dist/adapters/express.js +231 -220
  6. package/dist/{core/index.js → adapters/fetch.cjs} +276 -430
  7. package/dist/{core/index.cjs → adapters/fetch.js} +271 -447
  8. package/dist/adapters/node.cjs +251 -220
  9. package/dist/adapters/node.js +251 -221
  10. package/dist/adapters/socketio.cjs +198 -189
  11. package/dist/adapters/socketio.js +198 -190
  12. package/dist/adapters/websocket.cjs +201 -189
  13. package/dist/adapters/websocket.js +201 -190
  14. package/dist/dto/adapters/bind.d.ts +40 -0
  15. package/dist/dto/adapters/elysia.d.ts +1 -0
  16. package/dist/dto/adapters/express.d.ts +1 -0
  17. package/dist/dto/adapters/fetch.d.ts +1 -0
  18. package/dist/dto/adapters/http.d.ts +231 -0
  19. package/dist/dto/adapters/node.d.ts +1 -0
  20. package/dist/dto/adapters/realtime.d.ts +53 -0
  21. package/dist/dto/adapters/socketio.d.ts +1 -0
  22. package/dist/dto/adapters/websocket.d.ts +1 -0
  23. package/dist/{core/context.d.ts → dto/factories.d.ts} +6 -0
  24. package/dist/dto/index.cjs +734 -228
  25. package/dist/dto/index.d.ts +5 -6
  26. package/dist/dto/index.js +711 -229
  27. package/dist/dto/log/index.cjs +20 -39
  28. package/dist/dto/log/index.js +20 -39
  29. package/dist/dto/queue/index.cjs +20 -33
  30. package/dist/dto/queue/index.js +20 -33
  31. package/dist/dto/response/index.cjs +663 -44
  32. package/dist/dto/response/index.js +644 -45
  33. package/dist/dto/runtime/{dto.cjs → index.cjs} +544 -475
  34. package/dist/dto/runtime/index.d.ts +71 -0
  35. package/dist/dto/runtime/{dto.js → index.js} +541 -476
  36. package/dist/{core → dto/send}/builder.d.ts +9 -8
  37. package/dist/dto/send/index.d.ts +7 -0
  38. package/dist/{core → dto/send}/plugins.d.ts +2 -2
  39. package/dist/dto/send/presets.d.ts +34 -0
  40. package/dist/dto/send/transport.d.ts +50 -0
  41. package/dist/dto/{log/builder.d.ts → shapes/log.d.ts} +28 -12
  42. package/dist/dto/{queue/builder.d.ts → shapes/queue.d.ts} +36 -9
  43. package/dist/dto/{response/dto.d.ts → shapes/response.d.ts} +8 -6
  44. package/dist/{core/config.d.ts → dto/shared.d.ts} +5 -0
  45. package/dist/index.cjs +721 -614
  46. package/dist/index.d.ts +12 -9
  47. package/dist/index.js +704 -615
  48. package/dist/types/builder.d.ts +2 -1
  49. package/dist/types/log.d.ts +1 -1
  50. package/dist/types/queue.d.ts +1 -1
  51. package/package.json +59 -59
  52. package/dist/adapters/elysia.d.ts +0 -138
  53. package/dist/adapters/express.d.ts +0 -49
  54. package/dist/adapters/index.d.ts +0 -7
  55. package/dist/adapters/node.d.ts +0 -27
  56. package/dist/adapters/shared.d.ts +0 -11
  57. package/dist/adapters/socketio.d.ts +0 -31
  58. package/dist/adapters/websocket.d.ts +0 -31
  59. package/dist/core/assemble-log.d.ts +0 -1
  60. package/dist/core/assemble-queue.d.ts +0 -1
  61. package/dist/core/assemble.d.ts +0 -9
  62. package/dist/core/hooks.d.ts +0 -8
  63. package/dist/core/index.d.ts +0 -14
  64. package/dist/core/log-dto.d.ts +0 -1
  65. package/dist/core/metadata.d.ts +0 -6
  66. package/dist/core/presets.d.ts +0 -32
  67. package/dist/core/queue-dto.d.ts +0 -1
  68. package/dist/core/response-dto.d.ts +0 -1
  69. package/dist/core/schema-utils.d.ts +0 -1
  70. package/dist/dto/log/assemble.d.ts +0 -3
  71. package/dist/dto/log/index.d.ts +0 -3
  72. package/dist/dto/log/types.d.ts +0 -26
  73. package/dist/dto/queue/assemble.d.ts +0 -2
  74. package/dist/dto/queue/index.d.ts +0 -3
  75. package/dist/dto/queue/types.d.ts +0 -34
  76. package/dist/dto/response/assemble.d.ts +0 -1
  77. package/dist/dto/response/index.d.ts +0 -2
  78. package/dist/dto/runtime/dto.d.ts +0 -64
  79. package/dist/dto/shared/base-dto.d.ts +0 -12
  80. package/dist/dto/shared/schema-utils.d.ts +0 -1
  81. /package/dist/{enums/node-env.d.ts → utils/constants/enums.d.ts} +0 -0
package/README.md CHANGED
@@ -1,724 +1,600 @@
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
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 |
19
+ ### Three concepts
60
20
 
61
- All shape DTOs extend the same rules:
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 |
62
26
 
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)
27
+ **Rule of thumb:** client cevabı → `builder`. Düz JSON üretmek → static `DTO.*`.
66
28
 
67
- `Builder` is transport on top of `ResponseDTO` — same split as HTTP status vs JSON body.
29
+ ### 1. Wire the runtime (once)
68
30
 
69
- ---
31
+ ```typescript
32
+ import express from 'express'
33
+ import { DTO } from '@xrystal/dto'
70
34
 
71
- ## When to use what
35
+ const runtime = new DTO({ withDefaultHeaders: true })
36
+ const app = express()
37
+ app.use(runtime.express())
38
+ ```
72
39
 
73
- This is the most important distinction in the library.
40
+ This attaches `req.ctx` and `res.builder` on every request.
74
41
 
75
- ### You **have** a request (Express, Node, Elysia, WebSocket, Socket.IO)
42
+ ### 2. Respond in a handler
76
43
 
77
- Middleware from `new DTO()` attaches **`builder`** to the request/socket. Use that in handlers — it has context, dispatch, HTTP status, headers, and plugins.
44
+ Every chain ends with a **verb** — `.send()` (go to client) or `.build()` (schema only):
78
45
 
79
46
  ```typescript
80
- // Express / Node — res.builder
81
47
  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
- })
48
+ return res.builder.as.ok(
49
+ { id: req.params.id },
50
+ 'User fetched',
51
+ { meta: { requestId: req.ctx.requestId } },
52
+ ).send()
85
53
  })
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
54
  ```
99
55
 
100
- `DTO.Response` is **not** used here directly — `res.builder` already owns an internal `ResponseDTO`. You can inspect or reuse it:
56
+ Sent JSON (fixed order: `status → message → payload? → code`):
101
57
 
102
- ```typescript
103
- const bodyOnly = res.builder.dto.build() // same JSON shape, no dispatch yet
58
+ ```json
59
+ {
60
+ "status": true,
61
+ "message": "User fetched",
62
+ "payload": { "data": { "id": "42" }, "meta": { "requestId": "…" } },
63
+ "code": 0
64
+ }
104
65
  ```
105
66
 
106
- ### You **do not** have a request (cron, worker, unit test, logger, queue publisher)
67
+ | Verb | What it does |
68
+ |------|----------------|
69
+ | `.send()` | Assembles + **pushes** via send sink (HTTP write, WebSocket emit, …) |
70
+ | `.build()` | Assembles **schema only** — no network side-effect |
71
+
72
+ Presets (`.as.ok`, `.as.notFound`, …) configure the body and return a terminal — you still call `.send()` or `.build()`.
107
73
 
108
- No `req`, no `res`, no `ctx` — nothing to attach a builder to. Use **static shape DTOs**:
74
+ ### 3. Static shape DTOs (no request)
109
75
 
110
76
  ```typescript
111
- // Log line for your logger transport
112
- const logLine = DTO.Log.level('info')
113
- .service('billing-cron')
77
+ import { DTO, NodeEnvEnum } from '@xrystal/dto'
78
+
79
+ // Log line
80
+ DTO.Log.level('info')
81
+ .service('api')
114
82
  .env(NodeEnvEnum.PROD)
115
- .message('Invoices synced')
83
+ .message('Server ready')
116
84
  .build()
117
- logger.info(logLine)
118
85
 
119
- // Rabbit / SQS / Bull payload
120
- const message = DTO.Queue.source('billing-cron')
121
- .eventType('invoice.synced')
122
- .payload({ count: 42 })
86
+ // Queue message
87
+ DTO.Queue.source('api')
88
+ .eventType('user.created')
89
+ .payload({ id: 1 })
123
90
  .build()
124
- await queue.publish(message)
125
91
 
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()
92
+ // Response JSON only (no HTTP)
93
+ DTO.Response.status(true).message('OK').data({ id: 1 }).build()
131
94
  ```
132
95
 
133
- ### Summary
96
+ Every shape DTO shares the same chain rules:
134
97
 
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 |
98
+ - Fixed field order
99
+ - `.schema({ … })` for partial input — **last call wins**
100
+ - `payload` in `.schema()` **replaces** the entire payload
141
101
 
142
- ---
102
+ ### Elysia — same idea
143
103
 
144
- ## Response DTO
104
+ ```typescript
105
+ import { Elysia } from 'elysia'
106
+ import { DTO } from '@xrystal/dto'
107
+
108
+ const runtime = new DTO()
109
+ new Elysia()
110
+ .use(runtime.elysia())
111
+ .get('/users/:id', ({ builder, requestId, params }) =>
112
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }).send(),
113
+ )
114
+ ```
145
115
 
146
- Body only (`status → message → payload? → code`). No HTTP status here.
116
+ ### Fetch API — Workers, Hono, Bun.serve
147
117
 
148
118
  ```typescript
149
119
  import { DTO } from '@xrystal/dto'
150
120
 
151
- DTO.Response.status(true)
152
- .message('User fetched')
153
- .data({ id: 1 })
154
- .build()
121
+ const runtime = new DTO({ withDefaultHeaders: true })
155
122
 
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()
123
+ export async function GET(request: Request) {
124
+ const { builder, requestId } = runtime.forRequest(request)
125
+ return builder.as.ok({ ok: true }, 'Healthy', { meta: { requestId } }).send()
126
+ // → Web Response
127
+ }
163
128
  ```
164
129
 
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.
130
+ ### When to use what
166
131
 
167
- ---
132
+ | Situation | Use |
133
+ |-----------|-----|
134
+ | Express / Node / Elysia / Fetch handler | `builder…send()` |
135
+ | WebSocket / Socket.IO | `socket.builder…send()` |
136
+ | Schema only (log, queue, test, transform) | `builder…build()` or `DTO.*.build()` |
137
+ | Cron, worker, logger, queue publish | `DTO.Log`, `DTO.Queue`, `DTO.Response` |
138
+ | Custom HTTP status + headers | `builder.httpStatus(429).header(...).schema({...}).send()` |
168
139
 
169
- ## Log DTO
140
+ Plugins and custom `codes` on `new DTO({ … })` apply **only to `builder`** — not to static `DTO.Log` / `DTO.Queue`.
170
141
 
171
- Fixed field order: `level → service → message? → payload? → code? → timestamp → id? → env`
142
+ ---
172
143
 
173
- **Required at build:** `level`, `service`, `env`.
144
+ ## Adapters & send sinks
174
145
 
175
- ```typescript
176
- import { DTO, NodeEnvEnum } from '@xrystal/dto'
146
+ Each runtime has an **adapter** (wires `builder` per request) and a **send sink** (writes `FinalResult` to the transport on `.send()`).
177
147
 
178
- DTO.Log.level('info')
179
- .service('users-api')
180
- .env(NodeEnvEnum.PROD)
181
- .message('User fetched')
182
- .payload({ userId: 1 })
183
- .build()
148
+ | Piece | Role |
149
+ |-------|------|
150
+ | **Adapter** | Framework middleware / derive — creates `requestId`, attaches `builder`, applies default headers |
151
+ | **Send sink** | `{ dispatch(result) }` object — one line per runtime, delegates to `send/transport.ts` |
184
152
 
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()
153
+ ```
154
+ Handler chain Core Transport
155
+ ───────────── ───── ─────────
156
+ builder.as.ok() → .build() → FinalResult
157
+ → .send() → dispatchResult() → sink.dispatch()
158
+ → send/transport.ts
159
+ → res.json / Response / ws.send / …
193
160
  ```
194
161
 
195
- ---
162
+ Core dispatch lives in `dto/send/transport.ts`. Adapters pass a sink into `createBuilderRuntime()` — no separate dispatcher classes needed for normal use. `ExpressDispatcher` and friends remain as deprecated aliases for custom wiring.
196
163
 
197
- ## Queue DTO
164
+ ### Runtime map
198
165
 
199
- Shape: `{ metadata, payload }` — metadata order `source → eventType → timestamp → correlationId → userId? → extraData?`
166
+ | Runtime | Adapter | Sink | Attaches | `.send()` output |
167
+ |---------|---------|------|----------|------------------|
168
+ | **Express** | `ExpressAdapter` | `expressSink(res)` | `req.ctx`, `res.builder` | writes `res.status().json()` |
169
+ | **Node http** | `NodeAdapter` | `nodeSink(res)` | `req.ctx`, `res.builder` | writes `res.end(JSON)` + Set-Cookie |
170
+ | **Fetch API** | `FetchAdapter` | `fetchSink` | `builder`, `requestId` per `Request` | returns `Response` |
171
+ | **Elysia** | `ElysiaAdapter` | `elysiaSink(set)` | `builder`, `requestId`, `startTime` on ctx | mutates `set`, returns body |
172
+ | **WebSocket** | `WebSocketAdapter` | `websocketSink(socket)` | `socket.ctx`, `socket.builder` | `socket.send(wire envelope JSON)` |
173
+ | **Socket.IO** | `SocketIoAdapter` | `socketIoSink(socket, event)` | `socket.ctx`, `socket.builder` | `socket.emit(event, wire envelope)` |
200
174
 
201
- **Required at build:** `source`, `eventType`.
175
+ WebSocket and Socket.IO send the full transport envelope (`statusCode`, `headers`, `cookies`, `body`) so `.httpStatus()` and `.header()` are not lost on realtime transports.
176
+
177
+ ### `new DTO()` shortcuts
202
178
 
203
179
  ```typescript
204
- import { DTO } from '@xrystal/dto'
180
+ const runtime = new DTO({ withDefaultHeaders: true, socket: { event: 'api:response' } })
205
181
 
206
- DTO.Queue.source('users-api')
207
- .eventType('user.created')
208
- .userId('u-1')
209
- .payload({ id: 1 })
210
- .build()
182
+ runtime.express() // → Express middleware
183
+ runtime.node() // → Node http middleware
184
+ runtime.elysia() // → Elysia plugin
185
+ runtime.fetch() // → (request: Request) => { builder, requestId, … }
186
+ runtime.forRequest(request) // → one-shot Fetch context
211
187
 
212
- DTO.Queue.schema({
213
- metadata: { source: 'billing', eventType: 'invoice.paid' },
214
- payload: { invoiceId: 9 },
215
- }).build()
188
+ runtime.websocket() // → (socket, next?) => void
189
+ runtime.attachWebSocket(ws) // → attach builder to one socket
190
+
191
+ runtime.socketIo() // → (socket, next?) => void
192
+ runtime.attachSocketIo(socket) // → attach builder to one socket
216
193
  ```
217
194
 
218
- ---
195
+ ### When to import adapters directly
219
196
 
220
- ## Install
197
+ Most apps only need `new DTO().express()` (etc.). Import adapters / sinks when you:
221
198
 
222
- ```bash
223
- npm install @xrystal/dto
224
- ```
199
+ - Integrate a **custom framework** — `createBuilderRuntime(expressSink(res), options, context)`
200
+ - **Unit-test** transport without full middleware
201
+ - Build an **internal wrapper** (e.g. Fastify plugin)
225
202
 
226
- Peer dependencies (install only what you use):
203
+ ```typescript
204
+ import { createBuilderRuntime, expressSink, ContextFactory } from '@xrystal/dto'
227
205
 
228
- ```bash
229
- npm install express # Express adapter
230
- npm install elysia # Elysia adapter
206
+ // Custom wiring example
207
+ const context = ContextFactory.create(req)
208
+ const { builder } = createBuilderRuntime(expressSink(res), options, context)
231
209
  ```
232
210
 
233
- Subpath imports are available if you want a smaller surface:
211
+ Subpath imports (tree-shake friendly):
234
212
 
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
- ```
213
+ | Import | Contents |
214
+ |--------|----------|
215
+ | `@xrystal/dto/express` | `ExpressAdapter`, `ExpressDispatcher`, `expressMiddleware` |
216
+ | `@xrystal/dto/node` | `NodeAdapter`, `NodeDispatcher`, `nodeMiddleware` |
217
+ | `@xrystal/dto/fetch` | `FetchAdapter`, `FetchDispatcher`, `createFetchDerive` |
218
+ | `@xrystal/dto/elysia` | `ElysiaAdapter`, `ElysiaDispatcher`, `createElysiaDerive` |
219
+ | `@xrystal/dto/websocket` | `WebSocketAdapter`, `WebSocketDispatcher` |
220
+ | `@xrystal/dto/socketio` | `SocketIoAdapter`, `SocketIoDispatcher` |
243
221
 
244
222
  ---
245
223
 
246
- ## Quick start (Express)
224
+ ## Core vs DTO — tek adapter yeter mi?
247
225
 
248
- ```typescript
249
- import express from 'express'
250
- import { DTO } from '@xrystal/dto'
226
+ Kütüphanenin **iki işi** var; mimari buna göre bölündü:
251
227
 
252
- const app = express()
253
- app.use(new DTO().express())
228
+ | İş | Katman | Adapter gerekir mi? |
229
+ |----|--------|---------------------|
230
+ | **Şema üret** | `shapes/` + `shared.ts` | **Hayır** — `DTO.Log.build()`, `DTO.Response.build()`, testler |
231
+ | **Client'a gönder** | `send/` + `adapters/` | **Evet** — `.send()` bir transport'a yazmalı |
254
232
 
255
- app.get('/users/:id', (req, res) => {
256
- return res.builder.as.ok({ id: req.params.id }, 'User fetched')
257
- })
258
233
  ```
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
- }
234
+ ┌─────────────────────────────────────────────┐
235
+ │ new DTO() — tek giriş noktası │
236
+ │ runtime.express() / .fetch() / … │
237
+ └──────────────────┬──────────────────────────┘
238
+ │
239
+ ┌──────────────────▼──────────────────────────┐
240
+ │ adapters/ — sadece runtime wiring │
241
+ │ middleware → res.builder + send sink │
242
+ └──────────────────┬──────────────────────────┘
243
+ │
244
+ ┌──────────────────▼──────────────────────────┐
245
+ │ send/ — Builder, presets, transport │
246
+ │ .build() şema | .send() client'a yazar │
247
+ └──────────────────┬──────────────────────────┘
248
+ │
249
+ ┌──────────────────▼──────────────────────────┐
250
+ │ shapes/ — ResponseDTO, LogDTO, QueueDTO │
251
+ │ field sırası, .schema() ezme kuralları │
252
+ └─────────────────────────────────────────────┘
269
253
  ```
270
254
 
271
- Middleware attaches:
255
+ ### Tek adapter bağlamak yeter mi?
272
256
 
273
- - `req.ctx` — `{ requestId, startTime }`
274
- - `res.builder` — fluent response builder
275
-
276
- ---
257
+ | Senaryo | Ne yaparsın |
258
+ |---------|-------------|
259
+ | **Normal HTTP app (Express vb.)** | `new DTO().express()` — başka adapter import etmezsin |
260
+ | **Sadece JSON üret (log, queue, test)** | `DTO.Log` / `DTO.Queue` / `builder.build()` — adapter yok |
261
+ | **Custom framework (Fastify vb.)** | `createBuilderRuntime(expressSink(res), …)` veya kendi sink'in |
262
+ | **WS + HTTP aynı app'te** | Her transport için bir kez wire: `.express()` + `.websocket()` |
277
263
 
278
- ## Two ways to respond
264
+ **Özet:** Handler'dan client'a cevap gönderiyorsan bir adapter (veya sink) şart — ama çoğu app'te `new DTO().express()` tek satır yeter; `ExpressAdapter` import etmezsin. Dispatcher sınıfına gerek yok; adapter içinde `expressSink(res)` gibi tek satırlık `{ dispatch }` objesi var.
279
265
 
280
- ### 1. Presets (recommended for most handlers)
266
+ | Layer | Import eder misin? | Ne yapar |
267
+ |-------|-------------------|----------|
268
+ | **shapes** | Nadiren (`@xrystal/dto/log`) | Saf JSON şema |
269
+ | **send** | Nadiren (`@xrystal/dto/response`) | Builder, transport, presets |
270
+ | **adapters** | Custom entegrasyon / subpath | `builder`'ı request'e takar |
271
+ | **DTO** | Her zaman | Config + `runtime.express()` shortcut |
281
272
 
282
- Call `.as.*` — dispatches automatically. **Do not call `.build()`.**
273
+ ### Request lifecycle (Express)
283
274
 
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')
275
+ ```
276
+ 1. Request gelir
277
+ 2. ExpressAdapter middleware:
278
+ ContextFactory → requestId
279
+ createBuilderRuntime(expressSink(res), options, ctx)
280
+ res.builder = builder
281
+ 3. Handler:
282
+ res.builder.as.ok(data).send()
283
+ → .build() → FinalResult
284
+ → .send() → transport.ts → expressSink → res.json()
304
285
  ```
305
286
 
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()` |
287
+ Şema step 2'de oluşmaz — `.build()` veya `.send()` çağrıldığında assemble edilir.
313
288
 
314
- ### 2. Manual chain (headers, cookies, custom cases)
289
+ ---
315
290
 
316
- Use when you need transport control or a schema-shaped body. Always end with `.build()`.
291
+ ## Architecture
317
292
 
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
293
  ```
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()
294
+ src/dto/
295
+ ├── shared.ts BaseDTO, field rules, builder config
296
+ ├── factories.ts ContextFactory, MetadataFactory
297
+ ├── shapes/ ← görev 1: şema
298
+ │ ├── response.ts
299
+ │ ├── log.ts
300
+ │ └── queue.ts
301
+ ├── send/ ← görev 2: build + push
302
+ │ ├── builder.ts
303
+ │ ├── presets.ts
304
+ │ ├── transport.ts
305
+ │ └── plugins.ts
306
+ ├── adapters/ runtime wiring
307
+ │ ├── http.ts Express, Node, Fetch, Elysia + *Sink helpers
308
+ │ ├── realtime.ts WebSocket, Socket.IO
309
+ │ ├── bind.ts DTO.express() shortcuts
310
+ │ └── express.ts … @xrystal/dto/express subpath shims
311
+ └── runtime/
312
+ └── index.ts DTO class, Hooks, createBuilderRuntime
346
313
  ```
347
314
 
348
- **Chain rules**
315
+ Subpath exports (`@xrystal/dto/express`, `/response`, …) unchanged — shims map to the folders above.
349
316
 
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. |
317
+ ### Package exports
357
318
 
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 |
319
+ | Import | Contents |
320
+ |--------|----------|
321
+ | `@xrystal/dto` | Everything (including all adapters & dispatchers) |
322
+ | `@xrystal/dto/dto` | `DTO`, `createDTO`, runtime helpers |
323
+ | `@xrystal/dto/response` | `ResponseDTO`, `Builder`, `PresetTerminal`, transport helpers |
324
+ | `@xrystal/dto/log` | `LogDTO` |
325
+ | `@xrystal/dto/queue` | `QueueDTO` |
326
+ | `@xrystal/dto/express` | Express adapter |
327
+ | `@xrystal/dto/node` | Node http adapter |
328
+ | `@xrystal/dto/fetch` | Fetch API adapter |
329
+ | `@xrystal/dto/elysia` | Elysia adapter |
330
+ | `@xrystal/dto/websocket` | WebSocket adapter |
331
+ | `@xrystal/dto/socketio` | Socket.IO adapter |
369
332
 
370
333
  ---
371
334
 
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
- ```
335
+ ## Response schema
400
336
 
401
- Default codes (always merged; override only what you need):
337
+ | Field | Meaning |
338
+ |-------|---------|
339
+ | `status` | Business success/failure (`boolean`) — independent from HTTP status |
340
+ | `message` | Human-readable summary |
341
+ | `payload` | Optional wrapper (`data`, `meta`, pagination, …) |
342
+ | `code` | App code — default `0` SUCCESS, `1` CLIENT, `2` SERVER, `3` VALIDATION |
402
343
 
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 |
344
+ Transport (HTTP status, headers, cookies) lives on **`builder`**, not on `DTO.Response`.
409
345
 
410
346
  ---
411
347
 
412
- ## Core presets
348
+ ## Handler API
413
349
 
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 |
350
+ ### Presets (recommended)
422
351
 
423
- ---
352
+ Presets return a terminal — always end with `.send()` or `.build()`:
424
353
 
425
- ## HTTP-specific presets
354
+ ```typescript
355
+ return res.builder.as.ok(data, 'Done').send()
356
+ return res.builder.as.fail('Not found').send()
357
+ return res.builder.as.notFound('User missing').send()
358
+ return res.builder.as.tooManyRequests('Slow down').send()
426
359
 
427
- Use when you need an exact HTTP status. Core `.fail()` / `.error()` cover most cases.
360
+ // Headers before preset
361
+ return res.builder.header('X-Trace', traceId).as.ok(data).send()
362
+ ```
428
363
 
429
- ### Client (4xx)
364
+ ### Manual chain
430
365
 
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 |
366
+ ```typescript
367
+ return res.builder
368
+ .httpStatus(429)
369
+ .header('Retry-After', '60')
370
+ .schema({
371
+ status: false,
372
+ message: 'Slow down',
373
+ code: 4,
374
+ payload: { data: { retryAfter: 60 } },
375
+ })
376
+ .send()
377
+ ```
440
378
 
441
- ### Server (5xx)
379
+ Schema only (no dispatch):
442
380
 
443
- | Method | HTTP | Code |
444
- |--------|------|------|
445
- | `.internalServerError(…)` | 500 | SERVER |
446
- | `.serviceUnavailable(…)` | 503 | SERVER |
447
- | `.gatewayTimeout(…)` | 504 | SERVER |
381
+ ```typescript
382
+ const result = res.builder.schema({ payload: { data: user } }).build()
383
+ await auditLog.write(result.body)
384
+ ```
448
385
 
449
- All HTTP presets share the same argument shape as `.ok()` / `.fail()`.
386
+ | Chain rule | Detail |
387
+ |------------|--------|
388
+ | Last call wins | Later `.schema()` or `.status()` overrides earlier values |
389
+ | `.schema()` is body-only | HTTP status and headers stay on the chain |
390
+ | `payload` in `.schema()` | Replaces entire payload — no merge |
391
+ | Preset terminal | `.as.*()` exposes only `.build()` and `.send()` |
450
392
 
451
393
  ---
452
394
 
453
- ## Plugins
395
+ ## Shape DTOs
454
396
 
455
- Middleware-style pipeline. Lower `priority` runs first (default `0`).
397
+ ### Response (`DTO.Response`)
456
398
 
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 |
399
+ Body only — `status → message → payload? → code`.
463
400
 
464
401
  ```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
- })
402
+ DTO.Response.status(true).message('OK').data({ id: 1 }).build()
474
403
 
475
- new DTO({ plugins: [auditPlugin] })
476
- new DTO().use(auditPlugin, otherPlugin)
404
+ DTO.Response.schema({
405
+ status: false,
406
+ message: 'Not found',
407
+ code: 1,
408
+ }).build()
477
409
  ```
478
410
 
479
- ---
480
-
481
- ## Runtime guides
411
+ Inside a handler, `res.builder.dto` exposes the same `ResponseDTO` instance.
482
412
 
483
- ### Express
413
+ ### Log (`DTO.Log`)
484
414
 
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
- ```
415
+ Order: `level → service → message? → payload? → code? → timestamp → id? → env`
496
416
 
497
- ### Node HTTP
417
+ Required: `level`, `service`, `env`.
498
418
 
499
419
  ```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)
420
+ DTO.Log.level('info')
421
+ .service('users-api')
422
+ .env(NodeEnvEnum.PROD)
423
+ .message('User fetched')
424
+ .build()
511
425
  ```
512
426
 
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'
427
+ ### Queue (`DTO.Queue`)
520
428
 
521
- const dto = new DTO()
429
+ Order: `source → eventType → timestamp → correlationId → userId? → extraData?`
522
430
 
523
- createServer((req, res) => {
524
- dto.node()(req, res, () => {
525
- if (req.url === '/health') return res.builder?.as.ok({ ok: true }, 'Up')
431
+ Required: `source`, `eventType`. Timestamp and correlationId auto-generate.
526
432
 
527
- return res.builder
528
- ?.code(4)
529
- .httpStatus(429)
530
- .status(false)
531
- .message('Too many requests')
532
- .build()
533
- })
534
- }).listen(3000)
433
+ ```typescript
434
+ DTO.Queue.source('billing')
435
+ .eventType('invoice.paid')
436
+ .payload({ invoiceId: 9 })
437
+ .build()
535
438
  ```
536
439
 
537
- `req.ctx` holds `requestId`; `res.builder` is attached by middleware (use `?.` until the handler runs).
538
-
539
- ### Elysia
440
+ ---
540
441
 
541
- The handler argument **is** the context. The plugin adds `builder`, `requestId`, and `startTime` via scoped `derive`:
442
+ ## Runtime config
542
443
 
543
444
  ```typescript
544
- import { Elysia } from 'elysia'
545
- import { DTO } from '@xrystal/dto'
445
+ import { DTO, definePlugin } from '@xrystal/dto'
546
446
 
547
- const dto = new DTO()
447
+ const audit = definePlugin({
448
+ name: 'audit',
449
+ afterDispatch(result, ctx) {
450
+ console.log(ctx.context?.requestId, result.statusCode)
451
+ },
452
+ })
548
453
 
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
- )
454
+ const runtime = new DTO({
455
+ withDefaultHeaders: true,
456
+ codes: { RATE_LIMITED: 4 },
457
+ plugins: [audit],
458
+ socket: { event: 'api:response' },
459
+ }).use(otherPlugin) // immutable — returns new DTO
460
+
461
+ app.use(runtime.express())
462
+ io.use(runtime.socketIo())
557
463
  ```
558
464
 
559
- Outside a request (e.g. startup log):
465
+ | Option | Affects |
466
+ |--------|---------|
467
+ | `plugins` | `builder` pipeline only (`beforeDispatch` runs on `.send()`, not `.build()`) |
468
+ | `codes` | `builder` presets and `.code()` |
469
+ | `withDefaultHeaders` | HTTP / Fetch middleware |
470
+ | `socket.event` | Socket.IO emit name |
560
471
 
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
- ```
472
+ ---
570
473
 
571
- ### WebSocket
474
+ ## Runtimes
572
475
 
573
476
  ```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
- })
477
+ runtime.express()
478
+ runtime.node()
479
+ runtime.elysia()
480
+ runtime.fetch() / runtime.forRequest(request)
481
+ runtime.websocket() / runtime.attachWebSocket(ws)
482
+ runtime.socketIo() / runtime.attachSocketIo(socket)
589
483
  ```
590
484
 
591
- ### Socket.IO
485
+ ### Errors in handlers
592
486
 
593
487
  ```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')
488
+ try {
489
+ const user = await fetchUser(id)
490
+ return res.builder.as.ok(user, 'User fetched').send()
491
+ } catch (err) {
492
+ return res.builder.fromError(err).send()
493
+ }
599
494
  ```
600
495
 
601
- WebSocket and Socket.IO have no HTTP status — use presets or `.build()`. JSON is sent via `ws.send` / `socket.emit`.
496
+ Built-in: `ClientError`, `ServerError`, `ValidationError`.
602
497
 
603
498
  ---
604
499
 
605
- ## Full-stack example
500
+ ## Full-stack sketch
606
501
 
607
502
  ```typescript
608
503
  import express from 'express'
609
504
  import { DTO, NodeEnvEnum } from '@xrystal/dto'
610
505
 
611
- const runtime = new DTO({
612
- withDefaultHeaders: true,
613
- codes: { RATE_LIMITED: 4 },
614
- })
615
-
506
+ const runtime = new DTO({ withDefaultHeaders: true })
616
507
  const app = express()
617
508
  app.use(runtime.express())
618
509
 
619
510
  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()
511
+ const user = await db.findUser(req.params.id)
512
+ if (!user) return res.builder.as.notFound('User not found').send()
513
+
514
+ console.log(
515
+ DTO.Log.level('info')
516
+ .service('users-api')
517
+ .env(NodeEnvEnum.PROD)
518
+ .payload({ userId: user.id })
519
+ .build(),
520
+ )
648
521
 
649
- queue.publish(event)
650
- }, 60_000)
522
+ return res.builder.as.ok(user, 'User fetched', {
523
+ meta: { requestId: req.ctx.requestId },
524
+ }).send()
525
+ })
651
526
  ```
652
527
 
653
528
  ---
654
529
 
655
- ## Errors
530
+ ## HTTP presets reference
656
531
 
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
- ```
532
+ | Preset | HTTP | Code |
533
+ |--------|------|------|
534
+ | `.ok()` | 200 | SUCCESS |
535
+ | `.created()` | 201 | SUCCESS |
536
+ | `.fail()` | 400 | CLIENT |
537
+ | `.notFound()` | 404 | CLIENT |
538
+ | `.unprocessable()` | 422 | VALIDATION |
539
+ | `.tooManyRequests()` | 429 | CLIENT |
540
+ | `.error()` | 500 | SERVER |
667
541
 
668
- Built-in error types: `ClientError`, `ServerError`, `ValidationError`. Unknown errors are masked when `verbose: false`.
542
+ See source `dto/send/presets.ts` for the full list.
669
543
 
670
544
  ---
671
545
 
672
- ## Custom codes & HTTP status
546
+ ### Breaking / API changes
673
547
 
674
- ```typescript
675
- const dto = new DTO({ codes: { RATE_LIMITED: 4 } })
548
+ - **Explicit terminal verbs** — every handler chain must end with `.send()` (dispatch to client) or `.build()` (schema only). No automatic dispatch.
549
+ - **Preset terminal** — `.as.ok()`, `.as.fail()`, … return `PresetTerminal` with only `.build()` and `.send()`. No further chaining after presets (put headers **before** preset: `builder.header('X').as.ok(data).send()`).
550
+ - **WebSocket / Socket.IO wire format** — emits full transport envelope (`statusCode`, `headers`, `cookies`, `body`) instead of bare body JSON. Update clients if they parsed the old shape.
676
551
 
677
- return res.builder
678
- .code(4)
679
- .httpStatus(429)
680
- .status(false)
681
- .message('Too many requests')
682
- .build()
552
+ ### New
683
553
 
684
- // Or when HTTP maps cleanly:
685
- return res.builder.as.tooManyRequests('Too many requests')
686
- ```
554
+ - **Fetch API adapter** — `runtime.fetch()`, `runtime.forRequest(request)`, `@xrystal/dto/fetch`. Returns Web `Response` from `.send()`. Works with Workers, Hono, Bun.serve, Next.js route handlers.
555
+ - **Core transport layer** — `dto/send/transport.ts` centralises headers, cookies, JSON body, Fetch `Response`, and realtime wire envelope.
556
+ - **Exported adapters & dispatchers** — `ExpressDispatcher`, `FetchDispatcher`, `ElysiaDispatcher`, … available from main entry and subpaths for custom integrations.
687
557
 
688
- ---
558
+ ### Fixes & consistency
689
559
 
690
- ## Release (maintainers)
560
+ - **Elysia** — inline dispatcher replaced with `ElysiaDispatcher` class (same pattern as Express/Node/Fetch).
561
+ - **Node http** — `Set-Cookie` support added (was Express-only via `res.cookie`).
562
+ - **Dispatch hooks** — `beforeDispatch` / `afterDispatch` plugins run on `.send()` only, not on `.build()`.
563
+
564
+ ### Package surface
691
565
 
692
- **Source of truth:** `package.json` `"version"`. Git tag must be **identical** — no env override.
566
+ | Runtimes | 6 — Express, Node, Fetch, Elysia, WebSocket, Socket.IO |
567
+ | Tests | 77 passing |
568
+ | Subpaths | `/express`, `/node`, `/fetch`, `/elysia`, `/websocket`, `/socketio`, `/response`, `/log`, `/queue`, `/dto` |
693
569
 
694
- Format: `YY.MM.DD-vN` (e.g. `26.06.27-v8`).
570
+ ### Migration cheat sheet
695
571
 
696
- ### Quick tag (commit already done)
572
+ ```typescript
573
+ // Before
574
+ return res.builder.as.ok(data, 'Done')
697
575
 
698
- Bump `package.json`, commit, then:
576
+ // After
577
+ return res.builder.as.ok(data, 'Done').send()
699
578
 
700
- ```bash
701
- npm run tag
579
+ // Schema only (unchanged intent, now explicit)
580
+ const body = res.builder.as.ok(data).build()
581
+
582
+ // WS client — expect envelope now
583
+ // { statusCode, headers, cookies, body: { status, message, … } }
702
584
  ```
703
585
 
704
- Reads version from **committed** `package.json`, tags **current commit**, pushes branch + tag → Publish workflow runs.
586
+ ---
587
+
588
+ ## Release (maintainers)
705
589
 
706
- ### Full release (lint + test + build + commit + tag)
590
+ Version lives in `package.json` (`YY.MM.DD-vN`). Git tag must match exactly.
707
591
 
708
592
  ```bash
709
- # 1. Bump version in package.json
710
- # 2. Run everything:
711
- npm run release
593
+ npm run release # lint → test → build → commit → tag → push
594
+ npm run tag # tag only (after manual commit)
595
+ npm run version:check
712
596
  ```
713
597
 
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
598
  ---
723
599
 
724
600
  MIT