adorn-api 1.0.44 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,380 +1,422 @@
1
- # Adorn API
2
-
3
- A modern, decorator-first web framework built on Express with built-in OpenAPI 3.1 schema generation, designed for rapid API development with excellent Type safety and developer experience.
4
-
5
- ## Features
6
-
7
- - ✨ **Decorator-First API Definition**: Define controllers and DTOs with intuitive decorators
8
- - 📚 **Automatic OpenAPI 3.1 Generation**: API documentation is generated from your code
9
- - 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
10
- - 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
11
- - 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
1
+ # Adorn API
2
+
3
+ A modern, decorator-first web framework built on Express with built-in OpenAPI 3.1 schema generation, designed for rapid API development with excellent Type safety and developer experience.
4
+
5
+ ## Features
6
+
7
+ - ✨ **Decorator-First API Definition**: Define controllers and DTOs with intuitive decorators
8
+ - 📚 **Automatic OpenAPI 3.1 Generation**: API documentation is generated from your code
9
+ - 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
10
+ - 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
11
+ - 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
12
12
  - 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs, transformer-aware schema generation, and tree DTOs for nested set (MPTT) models
13
- - 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
14
- - 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
15
- - 🔧 **Transformers**: Custom field transformations with @Transform decorator and built-in transform functions
16
- - **Error Handling**: Structured error responses with error DTO support
17
- - 💾 **File Uploads**: Easy handling of file uploads with multipart form data
18
- - 🌐 **CORS Support**: Built-in CORS configuration
19
- - 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
20
-
21
- ## Installation
22
-
23
- ```bash
24
- npm install adorn-api
25
- ```
26
-
27
- Note: Adorn uses Stage 3 decorator metadata (`Symbol.metadata`). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import to keep decorator metadata consistent.
28
-
29
- ## Quick Start
30
-
31
- ### 1. Define DTOs
32
-
33
- ```typescript
34
- // user.dtos.ts
35
- import { Dto, Field, OmitDto, PickDto, t } from "adorn-api";
36
-
37
- @Dto({ description: "User record returned by the API." })
38
- export class UserDto {
39
- @Field(t.uuid({ description: "User identifier." }))
40
- id!: string;
41
-
42
- @Field(t.string({ minLength: 1 }))
43
- name!: string;
44
-
45
- @Field(t.optional(t.string()))
46
- nickname?: string;
47
- }
48
-
49
- @OmitDto(UserDto, ["id"])
50
- export class CreateUserDto {}
51
-
52
- @PickDto(UserDto, ["id"])
53
- export class UserParamsDto {}
54
- ```
55
-
56
- ### 2. Create a Controller
57
-
58
- ```typescript
59
- // user.controller.ts
60
- import {
61
- Body,
62
- Controller,
63
- Get,
64
- Params,
65
- Post,
66
- Returns,
67
- type RequestContext
68
- } from "adorn-api";
69
- import { CreateUserDto, UserDto, UserParamsDto } from "./user.dtos";
70
-
71
- @Controller("/users")
72
- export class UserController {
73
- @Get("/:id")
74
- @Params(UserParamsDto)
75
- @Returns(UserDto)
76
- async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
77
- return {
78
- id: ctx.params.id,
79
- name: "Ada Lovelace",
80
- nickname: "Ada"
81
- };
82
- }
83
-
84
- @Post("/")
85
- @Body(CreateUserDto)
86
- @Returns({ status: 201, schema: UserDto, description: "Created" })
87
- async create(ctx: RequestContext<CreateUserDto>) {
88
- return {
89
- id: "3f0f4d0f-1cb1-4cf1-9c32-3d4bce1b3f36",
90
- name: ctx.body.name,
91
- nickname: ctx.body.nickname
92
- };
93
- }
94
- }
95
- ```
96
-
97
- ### 3. Bootstrap the Application
98
-
99
- ```typescript
100
- // app.ts
101
- import { createExpressApp } from "adorn-api";
102
- import { UserController } from "./user.controller";
103
-
104
- export async function createApp() {
105
- return createExpressApp({
106
- controllers: [UserController],
107
- openApi: {
108
- info: {
109
- title: "Adorn API",
110
- version: "1.0.0"
111
- },
112
- docs: true
113
- }
114
- });
115
- }
116
-
117
- // index.ts
118
- import { createApp } from "./app";
119
-
120
- async function start() {
121
- const app = await createApp();
122
- const PORT = 3000;
123
-
124
- app.listen(PORT, () => {
125
- console.log(`Server running at http://localhost:${PORT}`);
126
- console.log(`OpenAPI documentation: http://localhost:${PORT}/openapi.json`);
127
- });
128
- }
129
-
130
- start().catch(error => {
131
- console.error("Failed to start server:", error);
132
- process.exit(1);
133
- });
134
- ```
135
-
136
- ## Core Concepts
137
-
138
- ### Controllers
139
-
140
- Controllers are classes decorated with `@Controller()` that group related API endpoints. Each controller has a base path and contains route handlers.
141
-
142
- ```typescript
143
- @Controller("/api/v1/users")
144
- export class UserController {
145
- // Routes go here
146
- }
147
- ```
148
-
149
- ### Routes
150
-
151
- Routes are methods decorated with HTTP verb decorators like `@Get()`, `@Post()`, `@Put()`, `@Patch()`, or `@Delete()`.
152
-
153
- ```typescript
154
- @Get("/:id")
155
- @Params(UserParamsDto)
156
- @Returns(UserDto)
157
- async getOne(ctx: RequestContext) {
158
- // Route handler logic
159
- }
160
- ```
161
-
162
- ### DTOs (Data Transfer Objects)
163
-
164
- DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
165
-
166
- ```typescript
167
- @Dto({ description: "User data" })
168
- export class UserDto {
169
- @Field(t.uuid({ description: "Unique identifier" }))
170
- id!: string;
171
-
172
- @Field(t.string({ minLength: 2, maxLength: 100 }))
173
- name!: string;
174
- }
175
- ```
176
-
177
- ### Stage 3 Decorator Metadata
178
-
179
- Adorn relies on Stage 3 decorator metadata (`Symbol.metadata`) to connect information across decorators (DTO fields, routes, params, etc.). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import so decorators share a consistent metadata object.
180
-
181
- ### Request Context
182
-
183
- Each route handler receives a `RequestContext` object that provides access to:
184
- - `ctx.body` - The request body (validated and typed)
185
- - `ctx.params` - Route parameters
186
- - `ctx.query` - Query parameters
187
- - `ctx.headers` - Request headers
188
- - `ctx.req` - The raw Express request
189
- - `ctx.res` - The raw Express response
190
- - `ctx.sse` - SSE emitter (for SSE routes)
191
- - `ctx.stream` - Streaming writer (for streaming routes)
192
-
193
- ## Advanced Features
194
-
195
- ### Server-Sent Events (SSE)
196
-
197
- ```typescript
198
- import { Controller, Get, Sse } from "adorn-api";
199
-
200
- @Controller("/events")
201
- class EventsController {
202
- @Get("/")
203
- @Sse({ description: "Real-time events stream" })
204
- async streamEvents(ctx: any) {
205
- const emitter = ctx.sse;
206
-
207
- let count = 0;
208
- const interval = setInterval(() => {
209
- count++;
210
- emitter.emit("message", {
211
- id: count,
212
- timestamp: new Date().toISOString(),
213
- message: `Event ${count}`
214
- });
215
-
216
- if (count >= 5) {
217
- clearInterval(interval);
218
- emitter.close();
219
- }
220
- }, 1000);
221
-
222
- ctx.req.on("close", () => {
223
- clearInterval(interval);
224
- emitter.close();
225
- });
226
- }
227
- }
228
- ```
229
-
230
- ### Streaming Responses
231
-
232
- ```typescript
233
- import { Controller, Get, Streaming } from "adorn-api";
234
-
235
- @Controller("/streaming")
236
- class StreamingController {
237
- @Get("/")
238
- @Streaming({ contentType: "text/plain" })
239
- async streamText(ctx: any) {
240
- const writer = ctx.stream;
241
- const data = ["First line", "Second line", "Third line"];
242
-
243
- for (let i = 0; i < data.length; i++) {
244
- writer.writeLine(data[i]);
245
- await new Promise(resolve => setTimeout(resolve, 500));
246
- }
247
-
248
- writer.close();
249
- }
250
- }
251
- ```
252
-
253
- ### File Uploads
254
-
255
- ```typescript
256
- import { Controller, Post, UploadedFile, Returns, t } from "adorn-api";
257
-
258
- @Controller("/uploads")
259
- class UploadController {
260
- @Post("/")
261
- @UploadedFile("file", t.file({ accept: ["image/*"], maxSize: 5 * 1024 * 1024 }))
262
- @Returns({ status: 200, schema: t.string() })
263
- async uploadFile(ctx: any) {
264
- const file = ctx.files?.file[0];
265
- return `File uploaded: ${file.originalname}`;
266
- }
267
- }
268
- ```
269
-
13
+ - 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
14
+ - 🔧 **Raw Responses**: Return binary data, files, and non-JSON content with the `@Raw` decorator
15
+ - 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
16
+ - 🔧 **Transformers**: Custom field transformations with @Transform decorator and built-in transform functions
17
+ - **Error Handling**: Structured error responses with error DTO support
18
+ - 💾 **File Uploads**: Easy handling of file uploads with multipart form data
19
+ - 🌐 **CORS Support**: Built-in CORS configuration
20
+ - 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install adorn-api
26
+ ```
27
+
28
+ Note: Adorn uses Stage 3 decorator metadata (`Symbol.metadata`). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import to keep decorator metadata consistent.
29
+
30
+ ## Quick Start
31
+
32
+ ### 1. Define DTOs
33
+
34
+ ```typescript
35
+ // user.dtos.ts
36
+ import { Dto, Field, OmitDto, PickDto, t } from "adorn-api";
37
+
38
+ @Dto({ description: "User record returned by the API." })
39
+ export class UserDto {
40
+ @Field(t.uuid({ description: "User identifier." }))
41
+ id!: string;
42
+
43
+ @Field(t.string({ minLength: 1 }))
44
+ name!: string;
45
+
46
+ @Field(t.optional(t.string()))
47
+ nickname?: string;
48
+ }
49
+
50
+ @OmitDto(UserDto, ["id"])
51
+ export class CreateUserDto {}
52
+
53
+ @PickDto(UserDto, ["id"])
54
+ export class UserParamsDto {}
55
+ ```
56
+
57
+ ### 2. Create a Controller
58
+
59
+ ```typescript
60
+ // user.controller.ts
61
+ import {
62
+ Body,
63
+ Controller,
64
+ Get,
65
+ Params,
66
+ Post,
67
+ Returns,
68
+ type RequestContext
69
+ } from "adorn-api";
70
+ import { CreateUserDto, UserDto, UserParamsDto } from "./user.dtos";
71
+
72
+ @Controller("/users")
73
+ export class UserController {
74
+ @Get("/:id")
75
+ @Params(UserParamsDto)
76
+ @Returns(UserDto)
77
+ async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
78
+ return {
79
+ id: ctx.params.id,
80
+ name: "Ada Lovelace",
81
+ nickname: "Ada"
82
+ };
83
+ }
84
+
85
+ @Post("/")
86
+ @Body(CreateUserDto)
87
+ @Returns({ status: 201, schema: UserDto, description: "Created" })
88
+ async create(ctx: RequestContext<CreateUserDto>) {
89
+ return {
90
+ id: "3f0f4d0f-1cb1-4cf1-9c32-3d4bce1b3f36",
91
+ name: ctx.body.name,
92
+ nickname: ctx.body.nickname
93
+ };
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### 3. Bootstrap the Application
99
+
100
+ ```typescript
101
+ // app.ts
102
+ import { createExpressApp } from "adorn-api";
103
+ import { UserController } from "./user.controller";
104
+
105
+ export async function createApp() {
106
+ return createExpressApp({
107
+ controllers: [UserController],
108
+ openApi: {
109
+ info: {
110
+ title: "Adorn API",
111
+ version: "1.0.0"
112
+ },
113
+ docs: true
114
+ }
115
+ });
116
+ }
117
+
118
+ // index.ts
119
+ import { createApp } from "./app";
120
+
121
+ async function start() {
122
+ const app = await createApp();
123
+ const PORT = 3000;
124
+
125
+ app.listen(PORT, () => {
126
+ console.log(`Server running at http://localhost:${PORT}`);
127
+ console.log(`OpenAPI documentation: http://localhost:${PORT}/openapi.json`);
128
+ });
129
+ }
130
+
131
+ start().catch(error => {
132
+ console.error("Failed to start server:", error);
133
+ process.exit(1);
134
+ });
135
+ ```
136
+
137
+ ## Core Concepts
138
+
139
+ ### Controllers
140
+
141
+ Controllers are classes decorated with `@Controller()` that group related API endpoints. Each controller has a base path and contains route handlers.
142
+
143
+ ```typescript
144
+ @Controller("/api/v1/users")
145
+ export class UserController {
146
+ // Routes go here
147
+ }
148
+ ```
149
+
150
+ ### Routes
151
+
152
+ Routes are methods decorated with HTTP verb decorators like `@Get()`, `@Post()`, `@Put()`, `@Patch()`, or `@Delete()`.
153
+
154
+ ```typescript
155
+ @Get("/:id")
156
+ @Params(UserParamsDto)
157
+ @Returns(UserDto)
158
+ async getOne(ctx: RequestContext) {
159
+ // Route handler logic
160
+ }
161
+ ```
162
+
163
+ ### DTOs (Data Transfer Objects)
164
+
165
+ DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
166
+
167
+ ```typescript
168
+ @Dto({ description: "User data" })
169
+ export class UserDto {
170
+ @Field(t.uuid({ description: "Unique identifier" }))
171
+ id!: string;
172
+
173
+ @Field(t.string({ minLength: 2, maxLength: 100 }))
174
+ name!: string;
175
+ }
176
+ ```
177
+
178
+ ### Stage 3 Decorator Metadata
179
+
180
+ Adorn relies on Stage 3 decorator metadata (`Symbol.metadata`) to connect information across decorators (DTO fields, routes, params, etc.). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import so decorators share a consistent metadata object.
181
+
182
+ ### Request Context
183
+
184
+ Each route handler receives a `RequestContext` object that provides access to:
185
+ - `ctx.body` - The request body (validated and typed)
186
+ - `ctx.params` - Route parameters
187
+ - `ctx.query` - Query parameters
188
+ - `ctx.headers` - Request headers
189
+ - `ctx.req` - The raw Express request
190
+ - `ctx.res` - The raw Express response
191
+ - `ctx.sse` - SSE emitter (for SSE routes)
192
+ - `ctx.stream` - Streaming writer (for streaming routes)
193
+
194
+ ## Advanced Features
195
+
196
+ ### Server-Sent Events (SSE)
197
+
198
+ ```typescript
199
+ import { Controller, Get, Sse } from "adorn-api";
200
+
201
+ @Controller("/events")
202
+ class EventsController {
203
+ @Get("/")
204
+ @Sse({ description: "Real-time events stream" })
205
+ async streamEvents(ctx: any) {
206
+ const emitter = ctx.sse;
207
+
208
+ let count = 0;
209
+ const interval = setInterval(() => {
210
+ count++;
211
+ emitter.emit("message", {
212
+ id: count,
213
+ timestamp: new Date().toISOString(),
214
+ message: `Event ${count}`
215
+ });
216
+
217
+ if (count >= 5) {
218
+ clearInterval(interval);
219
+ emitter.close();
220
+ }
221
+ }, 1000);
222
+
223
+ ctx.req.on("close", () => {
224
+ clearInterval(interval);
225
+ emitter.close();
226
+ });
227
+ }
228
+ }
229
+ ```
230
+
231
+ ### Streaming Responses
232
+
233
+ ```typescript
234
+ import { Controller, Get, Streaming } from "adorn-api";
235
+
236
+ @Controller("/streaming")
237
+ class StreamingController {
238
+ @Get("/")
239
+ @Streaming({ contentType: "text/plain" })
240
+ async streamText(ctx: any) {
241
+ const writer = ctx.stream;
242
+ const data = ["First line", "Second line", "Third line"];
243
+
244
+ for (let i = 0; i < data.length; i++) {
245
+ writer.writeLine(data[i]);
246
+ await new Promise(resolve => setTimeout(resolve, 500));
247
+ }
248
+
249
+ writer.close();
250
+ }
251
+ }
252
+ ```
253
+
254
+ ### Raw Responses
255
+
256
+ Use the `@Raw()` decorator to return binary data (files, images, PDFs, etc.) without JSON serialization. The response body is sent with `res.send()` instead of `res.json()`.
257
+
258
+ ```typescript
259
+ import { Controller, Get, Raw, Params, ok, type RequestContext } from "adorn-api";
260
+ import fs from "fs/promises";
261
+
262
+ @Controller("/files")
263
+ class FileController {
264
+ @Get("/report.pdf")
265
+ @Raw({ contentType: "application/pdf", description: "Download PDF report" })
266
+ async downloadPdf(ctx: RequestContext) {
267
+ const buffer = await fs.readFile("report.pdf");
268
+ return ok(buffer);
269
+ }
270
+
271
+ @Get("/avatar/:id")
272
+ @Raw({ contentType: "image/png" })
273
+ async getAvatar(ctx: RequestContext) {
274
+ const image = await fs.readFile(`avatars/${ctx.params.id}.png`);
275
+ return image;
276
+ }
277
+ }
278
+ ```
279
+
280
+ You can also set custom headers (e.g. `Content-Disposition`) via `HttpResponse`:
281
+
282
+ ```typescript
283
+ import { HttpResponse } from "adorn-api";
284
+
285
+ @Get("/download/:filename")
286
+ @Raw({ contentType: "application/octet-stream" })
287
+ async download(ctx: RequestContext) {
288
+ const buffer = await fs.readFile(`uploads/${ctx.params.filename}`);
289
+ return new HttpResponse(200, buffer, {
290
+ "Content-Disposition": `attachment; filename="${ctx.params.filename}"`
291
+ });
292
+ }
293
+ ```
294
+
295
+ ### File Uploads
296
+
297
+ ```typescript
298
+ import { Controller, Post, UploadedFile, Returns, t } from "adorn-api";
299
+
300
+ @Controller("/uploads")
301
+ class UploadController {
302
+ @Post("/")
303
+ @UploadedFile("file", t.file({ accept: ["image/*"], maxSize: 5 * 1024 * 1024 }))
304
+ @Returns({ status: 200, schema: t.string() })
305
+ async uploadFile(ctx: any) {
306
+ const file = ctx.files?.file[0];
307
+ return `File uploaded: ${file.originalname}`;
308
+ }
309
+ }
310
+ ```
311
+
270
312
  ## Metal ORM Integration
271
-
272
- Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
273
- Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
274
-
275
- ### 1. Define Entities
276
-
277
- ```typescript
278
- // user.entity.ts
279
- import { Entity, PrimaryKey, Property } from "metal-orm";
280
-
281
- @Entity("users")
282
- export class User {
283
- @PrimaryKey()
284
- id!: number;
285
-
286
- @Property()
287
- name!: string;
288
-
289
- @Property({ nullable: true })
290
- nickname?: string;
291
- }
292
- ```
293
-
294
- ### 2. Generate CRUD DTOs
295
-
296
- ```typescript
297
- // user.dtos.ts
298
- import { createMetalCrudDtoClasses } from "adorn-api";
299
- import { User } from "./user.entity";
300
-
301
- export const {
302
- GetUserDto,
303
- CreateUserDto,
304
- UpdateUserDto,
305
- ReplaceUserDto,
306
- UserQueryDto,
307
- UserPagedResponseDto
308
- } = createMetalCrudDtoClasses(User);
309
- ```
310
-
313
+
314
+ Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
315
+ Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
316
+
317
+ ### 1. Define Entities
318
+
319
+ ```typescript
320
+ // user.entity.ts
321
+ import { Entity, PrimaryKey, Property } from "metal-orm";
322
+
323
+ @Entity("users")
324
+ export class User {
325
+ @PrimaryKey()
326
+ id!: number;
327
+
328
+ @Property()
329
+ name!: string;
330
+
331
+ @Property({ nullable: true })
332
+ nickname?: string;
333
+ }
334
+ ```
335
+
336
+ ### 2. Generate CRUD DTOs
337
+
338
+ ```typescript
339
+ // user.dtos.ts
340
+ import { createMetalCrudDtoClasses } from "adorn-api";
341
+ import { User } from "./user.entity";
342
+
343
+ export const {
344
+ GetUserDto,
345
+ CreateUserDto,
346
+ UpdateUserDto,
347
+ ReplaceUserDto,
348
+ UserQueryDto,
349
+ UserPagedResponseDto
350
+ } = createMetalCrudDtoClasses(User);
351
+ ```
352
+
311
353
  ### 3. Create a CRUD Controller
312
354
 
313
355
  ```typescript
314
356
  // user.controller.ts
315
- import {
316
- Controller,
317
- Get,
318
- Post,
319
- Put,
320
- Patch,
321
- Delete,
322
- Params,
323
- Body,
324
- Query,
325
- Returns,
326
- parsePagination,
327
- type RequestContext
328
- } from "adorn-api";
329
- import { applyFilter, toPagedResponse } from "metal-orm";
330
- import { createSession } from "./db";
331
- import { User } from "./user.entity";
332
- import {
333
- GetUserDto,
334
- CreateUserDto,
335
- UpdateUserDto,
336
- ReplaceUserDto,
337
- UserQueryDto,
338
- UserPagedResponseDto
339
- } from "./user.dtos";
340
-
341
- @Controller("/users")
342
- export class UserController {
343
- @Get("/")
344
- @Query(UserQueryDto)
345
- @Returns(UserPagedResponseDto)
346
- async list(ctx: RequestContext<unknown, UserQueryDto>) {
347
- const { page, pageSize } = parsePagination(ctx.query);
348
- const session = createSession();
349
-
350
- try {
351
- const query = applyFilter(
352
- User.select().orderBy(User.id, "ASC"),
353
- User,
354
- ctx.query
355
- );
356
-
357
- const paged = await query.executePaged(session, { page, pageSize });
358
- return toPagedResponse(paged);
359
- } finally {
360
- await session.dispose();
361
- }
362
- }
363
-
364
- @Get("/:id")
365
- @Params({ id: t.integer() })
366
- @Returns(GetUserDto)
367
- async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
368
- const session = createSession();
369
-
370
- try {
371
- const user = await session.find(User, parseInt(ctx.params.id));
372
- return user;
373
- } finally {
374
- await session.dispose();
375
- }
376
- }
377
-
357
+ import {
358
+ Controller,
359
+ Get,
360
+ Post,
361
+ Put,
362
+ Patch,
363
+ Delete,
364
+ Params,
365
+ Body,
366
+ Query,
367
+ Returns,
368
+ parsePagination,
369
+ type RequestContext
370
+ } from "adorn-api";
371
+ import { applyFilter, toPagedResponse } from "metal-orm";
372
+ import { createSession } from "./db";
373
+ import { User } from "./user.entity";
374
+ import {
375
+ GetUserDto,
376
+ CreateUserDto,
377
+ UpdateUserDto,
378
+ ReplaceUserDto,
379
+ UserQueryDto,
380
+ UserPagedResponseDto
381
+ } from "./user.dtos";
382
+
383
+ @Controller("/users")
384
+ export class UserController {
385
+ @Get("/")
386
+ @Query(UserQueryDto)
387
+ @Returns(UserPagedResponseDto)
388
+ async list(ctx: RequestContext<unknown, UserQueryDto>) {
389
+ const { page, pageSize } = parsePagination(ctx.query);
390
+ const session = createSession();
391
+
392
+ try {
393
+ const query = applyFilter(
394
+ User.select().orderBy(User.id, "ASC"),
395
+ User,
396
+ ctx.query
397
+ );
398
+
399
+ const paged = await query.executePaged(session, { page, pageSize });
400
+ return toPagedResponse(paged);
401
+ } finally {
402
+ await session.dispose();
403
+ }
404
+ }
405
+
406
+ @Get("/:id")
407
+ @Params({ id: t.integer() })
408
+ @Returns(GetUserDto)
409
+ async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
410
+ const session = createSession();
411
+
412
+ try {
413
+ const user = await session.find(User, parseInt(ctx.params.id));
414
+ return user;
415
+ } finally {
416
+ await session.dispose();
417
+ }
418
+ }
419
+
378
420
  // Other CRUD operations...
379
421
  }
380
422
  ```
@@ -523,395 +565,395 @@ class CategoryController {
523
565
  }
524
566
  }
525
567
  ```
526
-
527
- ## Configuration
528
-
529
- ### Express App Options
530
-
531
- ```typescript
532
- createExpressApp({
533
- // Required
534
- controllers: [UserController],
535
-
536
- // Optional
537
- cors: true, // Enable CORS with default options or configure
538
- jsonBody: true, // Parse JSON bodies (default: true)
539
- inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
540
- validation: { // Validation configuration
541
- enabled: true, // Enable validation (default: true)
542
- mode: "strict" // Validation mode: "strict" or "safe"
543
- },
544
- multipart: { // File upload configuration
545
- dest: "./uploads",
546
- limits: { fileSize: 50 * 1024 * 1024 }
547
- },
548
- openApi: {
549
- info: {
550
- title: "My API",
551
- version: "1.0.0",
552
- description: "API documentation"
553
- },
554
- path: "/openapi.json", // OpenAPI schema endpoint
555
- docs: true // Serve Swagger UI
556
- }
557
- });
558
- ```
559
-
560
- ## Schema Types
561
-
562
- The `t` object provides a rich set of schema types:
563
-
564
- - Primitives: `t.string()`, `t.number()`, `t.integer()`, `t.boolean()`
565
- - Formats: `t.uuid()`, `t.dateTime()`
566
- - Complex: `t.array()`, `t.object()`, `t.record()`
567
- - Combinators: `t.union()`, `t.enum()`, `t.literal()`
568
- - Special: `t.ref()`, `t.any()`, `t.null()`, `t.file()`
569
- - Modifiers: `t.optional()`, `t.nullable()`
570
-
571
- ## DTO Composition
572
-
573
- Reuse and compose DTOs with these decorators:
574
-
575
- ```typescript
576
- // Pick specific fields from an existing DTO
577
- @PickDto(UserDto, ["id", "name"])
578
- export class UserSummaryDto {}
579
-
580
- // Omit specific fields from an existing DTO
581
- @OmitDto(UserDto, ["password"])
582
- export class PublicUserDto {}
583
-
584
- // Make all fields optional
585
- @PartialDto(UserDto)
586
- export class UpdateUserDto {}
587
-
588
- // Merge multiple DTOs
589
- @MergeDto([UserDto, AddressDto])
590
- export class UserWithAddressDto {}
591
- ```
592
-
593
- ## Error Handling
594
-
595
- Define structured error responses:
596
-
597
- ```typescript
598
- import { Controller, Get, ReturnsError, t } from "adorn-api";
599
-
600
- @Controller("/")
601
- class ErrorController {
602
- @Get("/error")
603
- @ReturnsError({
604
- status: 404,
605
- schema: t.object({
606
- code: t.string(),
607
- message: t.string(),
608
- details: t.optional(t.record(t.any()))
609
- }),
610
- description: "Resource not found"
611
- })
612
- async notFound() {
613
- throw new HttpError(404, "Resource not found", { code: "NOT_FOUND" });
614
- }
615
- }
616
- ```
617
-
618
- ## Lifecycle Hooks
619
-
620
- ```typescript
621
- import {
622
- OnApplicationBootstrap,
623
- OnShutdown
624
- } from "adorn-api";
625
-
626
- class DatabaseService implements OnApplicationBootstrap, OnShutdown {
627
- async onApplicationBootstrap() {
628
- console.log("Connecting to database...");
629
- // Initialize database connection
630
- }
631
-
632
- async onShutdown(signal?: string) {
633
- console.log(`Shutting down (${signal})...`);
634
- // Cleanup resources
635
- }
636
- }
637
-
638
- // Register the service
639
- import { lifecycleRegistry } from "adorn-api";
640
- lifecycleRegistry.register(new DatabaseService());
641
- ```
642
-
643
- ## Validation
644
-
645
- Adorn API provides automatic request validation and a comprehensive validation system for your DTOs and schemas.
646
-
647
- ### Validation Configuration
648
-
649
- ```typescript
650
- createExpressApp({
651
- controllers: [UserController],
652
- validation: {
653
- enabled: true, // Enable validation (default: true)
654
- mode: "strict" // Validation mode: "strict" or "safe"
655
- }
656
- });
657
- ```
658
-
659
- ### Validation Errors
660
-
661
- Invalid requests automatically return structured validation errors:
662
-
663
- ```typescript
664
- // Example error response
665
- {
666
- "statusCode": 400,
667
- "message": "Validation failed",
668
- "errors": [
669
- {
670
- "field": "name",
671
- "message": "must be at least 1 character long",
672
- "value": "",
673
- "code": "STRING_MIN_LENGTH"
674
- },
675
- {
676
- "field": "email",
677
- "message": "must be a valid email",
678
- "value": "invalid-email",
679
- "code": "FORMAT_EMAIL"
680
- }
681
- ]
682
- }
683
- ```
684
-
685
- ### Validation Error Codes
686
-
687
- Adorn API provides machine-readable error codes for programmatic error handling:
688
-
689
- ```typescript
690
- import { ValidationErrorCode } from "adorn-api";
691
-
692
- console.log(ValidationErrorCode.FORMAT_EMAIL); // "FORMAT_EMAIL"
693
- console.log(ValidationErrorCode.STRING_MIN_LENGTH); // "STRING_MIN_LENGTH"
694
- ```
695
-
696
- ### Manual Validation
697
-
698
- You can also manually validate data using the `validate` function:
699
-
700
- ```typescript
701
- import { validate, ValidationErrors, t } from "adorn-api";
702
-
703
- const data = { name: "", email: "invalid" };
704
- const errors = validate(data, t.object({
705
- name: t.string({ minLength: 1 }),
706
- email: t.string({ format: "email" })
707
- }));
708
-
709
- if (errors.length > 0) {
710
- throw new ValidationErrors(errors);
711
- }
712
- ```
713
-
714
- ## Transformers
715
-
716
- Transform fields during serialization with custom transform functions or built-in transform utilities.
717
-
718
- ### Basic Transform
719
-
720
- ```typescript
721
- import { Dto, Field, Transform, t } from "adorn-api";
722
-
723
- @Dto()
724
- export class UserDto {
725
- @Field(t.string())
726
- @Transform((value) => value.toUpperCase())
727
- name!: string;
728
-
729
- @Field(t.dateTime())
730
- @Transform((value) => value.toISOString())
731
- createdAt!: Date;
732
- }
733
- ```
734
-
735
- ### Built-in Transforms
736
-
737
- Adorn API includes common transform functions:
738
-
739
- ```typescript
740
- import { Dto, Field, Transform, Transforms, t } from "adorn-api";
741
-
742
- @Dto()
743
- export class UserDto {
744
- @Field(t.string())
745
- @Transform(Transforms.toLowerCase)
746
- email!: string;
747
-
748
- @Field(t.number())
749
- @Transform(Transforms.round(2))
750
- price!: number;
751
-
752
- @Field(t.string())
753
- @Transform(Transforms.mask(4)) // Mask all but last 4 characters
754
- creditCard!: string;
755
-
756
- @Field(t.dateTime())
757
- @Transform(Transforms.toISOString)
758
- birthDate!: Date;
759
- }
760
- ```
761
-
762
- ### Conditional Transforms with Groups
763
-
764
- Apply transforms only to specific serialization groups:
765
-
766
- ```typescript
767
- import { Dto, Field, Transform, Expose, t } from "adorn-api";
768
-
769
- @Dto()
770
- export class UserDto {
771
- @Field(t.string())
772
- name!: string;
773
-
774
- @Field(t.string())
775
- @Expose({ groups: ["admin"] })
776
- @Transform((value) => Transforms.mask(2), { groups: ["admin"] })
777
- phoneNumber!: string;
778
-
779
- @Field(t.string())
780
- @Expose({ groups: ["internal"] })
781
- @Transform((value) => "[REDACTED]", { groups: ["external"] })
782
- internalNote!: string;
783
- }
784
- ```
785
-
786
- ### Custom Transform Functions
787
-
788
- Create custom transform functions:
789
-
790
- ```typescript
791
- import { Dto, Field, Transform, t } from "adorn-api";
792
-
793
- const toCurrency = (value: number, currency: string = "USD") => {
794
- return new Intl.NumberFormat("en-US", {
795
- style: "currency",
796
- currency
797
- }).format(value);
798
- };
799
-
800
- @Dto()
801
- export class ProductDto {
802
- @Field(t.string())
803
- name!: string;
804
-
805
- @Field(t.number())
806
- @Transform(toCurrency)
807
- price!: number;
808
-
809
- @Field(t.number())
810
- @Transform((value) => toCurrency(value, "EUR"))
811
- priceEUR!: number;
812
- }
813
- ```
814
-
815
- ### Serialization with Options
816
-
817
- Control serialization with custom options:
818
-
819
- ```typescript
820
- import { serialize, createSerializer } from "adorn-api";
821
- import { UserDto } from "./user.dtos";
822
-
823
- const user = new UserDto();
824
- user.name = "John Doe";
825
- user.phoneNumber = "123-456-7890";
826
- user.internalNote = "This is an internal note";
827
-
828
- // Basic serialization
829
- const basic = serialize(user);
830
- // Output: { name: "John Doe" }
831
-
832
- // Admin group serialization
833
- const admin = serialize(user, { groups: ["admin"] });
834
- // Output: { name: "John Doe", phoneNumber: "********90" }
835
-
836
- // External group serialization
837
- const external = serialize(user, { groups: ["external"] });
838
- // Output: { name: "John Doe", internalNote: "[REDACTED]" }
839
-
840
- // Create a preset serializer
841
- const adminSerializer = createSerializer({ groups: ["admin"] });
842
- const adminData = adminSerializer(user);
843
- ```
844
-
845
- ### Exclude Fields
846
-
847
- Control which fields are excluded or exposed:
848
-
849
- ```typescript
850
- import { Dto, Field, Exclude, Expose, t } from "adorn-api";
851
-
852
- @Dto()
853
- export class UserDto {
854
- @Field(t.string())
855
- name!: string;
856
-
857
- @Field(t.string())
858
- @Exclude() // Always exclude from serialization
859
- password!: string;
860
-
861
- @Field(t.string())
862
- @Expose({ name: "email_address" }) // Rename field in output
863
- email!: string;
864
-
865
- @Field(t.string())
866
- @Exclude({ groups: ["public"] }) // Exclude from public group
867
- internalComment!: string;
868
- }
869
- ```
870
-
568
+
569
+ ## Configuration
570
+
571
+ ### Express App Options
572
+
573
+ ```typescript
574
+ createExpressApp({
575
+ // Required
576
+ controllers: [UserController],
577
+
578
+ // Optional
579
+ cors: true, // Enable CORS with default options or configure
580
+ jsonBody: true, // Parse JSON bodies (default: true)
581
+ inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
582
+ validation: { // Validation configuration
583
+ enabled: true, // Enable validation (default: true)
584
+ mode: "strict" // Validation mode: "strict" or "safe"
585
+ },
586
+ multipart: { // File upload configuration
587
+ dest: "./uploads",
588
+ limits: { fileSize: 50 * 1024 * 1024 }
589
+ },
590
+ openApi: {
591
+ info: {
592
+ title: "My API",
593
+ version: "1.0.0",
594
+ description: "API documentation"
595
+ },
596
+ path: "/openapi.json", // OpenAPI schema endpoint
597
+ docs: true // Serve Swagger UI
598
+ }
599
+ });
600
+ ```
601
+
602
+ ## Schema Types
603
+
604
+ The `t` object provides a rich set of schema types:
605
+
606
+ - Primitives: `t.string()`, `t.number()`, `t.integer()`, `t.boolean()`
607
+ - Formats: `t.uuid()`, `t.dateTime()`
608
+ - Complex: `t.array()`, `t.object()`, `t.record()`
609
+ - Combinators: `t.union()`, `t.enum()`, `t.literal()`
610
+ - Special: `t.ref()`, `t.any()`, `t.null()`, `t.file()`
611
+ - Modifiers: `t.optional()`, `t.nullable()`
612
+
613
+ ## DTO Composition
614
+
615
+ Reuse and compose DTOs with these decorators:
616
+
617
+ ```typescript
618
+ // Pick specific fields from an existing DTO
619
+ @PickDto(UserDto, ["id", "name"])
620
+ export class UserSummaryDto {}
621
+
622
+ // Omit specific fields from an existing DTO
623
+ @OmitDto(UserDto, ["password"])
624
+ export class PublicUserDto {}
625
+
626
+ // Make all fields optional
627
+ @PartialDto(UserDto)
628
+ export class UpdateUserDto {}
629
+
630
+ // Merge multiple DTOs
631
+ @MergeDto([UserDto, AddressDto])
632
+ export class UserWithAddressDto {}
633
+ ```
634
+
635
+ ## Error Handling
636
+
637
+ Define structured error responses:
638
+
639
+ ```typescript
640
+ import { Controller, Get, ReturnsError, t } from "adorn-api";
641
+
642
+ @Controller("/")
643
+ class ErrorController {
644
+ @Get("/error")
645
+ @ReturnsError({
646
+ status: 404,
647
+ schema: t.object({
648
+ code: t.string(),
649
+ message: t.string(),
650
+ details: t.optional(t.record(t.any()))
651
+ }),
652
+ description: "Resource not found"
653
+ })
654
+ async notFound() {
655
+ throw new HttpError(404, "Resource not found", { code: "NOT_FOUND" });
656
+ }
657
+ }
658
+ ```
659
+
660
+ ## Lifecycle Hooks
661
+
662
+ ```typescript
663
+ import {
664
+ OnApplicationBootstrap,
665
+ OnShutdown
666
+ } from "adorn-api";
667
+
668
+ class DatabaseService implements OnApplicationBootstrap, OnShutdown {
669
+ async onApplicationBootstrap() {
670
+ console.log("Connecting to database...");
671
+ // Initialize database connection
672
+ }
673
+
674
+ async onShutdown(signal?: string) {
675
+ console.log(`Shutting down (${signal})...`);
676
+ // Cleanup resources
677
+ }
678
+ }
679
+
680
+ // Register the service
681
+ import { lifecycleRegistry } from "adorn-api";
682
+ lifecycleRegistry.register(new DatabaseService());
683
+ ```
684
+
685
+ ## Validation
686
+
687
+ Adorn API provides automatic request validation and a comprehensive validation system for your DTOs and schemas.
688
+
689
+ ### Validation Configuration
690
+
691
+ ```typescript
692
+ createExpressApp({
693
+ controllers: [UserController],
694
+ validation: {
695
+ enabled: true, // Enable validation (default: true)
696
+ mode: "strict" // Validation mode: "strict" or "safe"
697
+ }
698
+ });
699
+ ```
700
+
701
+ ### Validation Errors
702
+
703
+ Invalid requests automatically return structured validation errors:
704
+
705
+ ```typescript
706
+ // Example error response
707
+ {
708
+ "statusCode": 400,
709
+ "message": "Validation failed",
710
+ "errors": [
711
+ {
712
+ "field": "name",
713
+ "message": "must be at least 1 character long",
714
+ "value": "",
715
+ "code": "STRING_MIN_LENGTH"
716
+ },
717
+ {
718
+ "field": "email",
719
+ "message": "must be a valid email",
720
+ "value": "invalid-email",
721
+ "code": "FORMAT_EMAIL"
722
+ }
723
+ ]
724
+ }
725
+ ```
726
+
727
+ ### Validation Error Codes
728
+
729
+ Adorn API provides machine-readable error codes for programmatic error handling:
730
+
731
+ ```typescript
732
+ import { ValidationErrorCode } from "adorn-api";
733
+
734
+ console.log(ValidationErrorCode.FORMAT_EMAIL); // "FORMAT_EMAIL"
735
+ console.log(ValidationErrorCode.STRING_MIN_LENGTH); // "STRING_MIN_LENGTH"
736
+ ```
737
+
738
+ ### Manual Validation
739
+
740
+ You can also manually validate data using the `validate` function:
741
+
742
+ ```typescript
743
+ import { validate, ValidationErrors, t } from "adorn-api";
744
+
745
+ const data = { name: "", email: "invalid" };
746
+ const errors = validate(data, t.object({
747
+ name: t.string({ minLength: 1 }),
748
+ email: t.string({ format: "email" })
749
+ }));
750
+
751
+ if (errors.length > 0) {
752
+ throw new ValidationErrors(errors);
753
+ }
754
+ ```
755
+
756
+ ## Transformers
757
+
758
+ Transform fields during serialization with custom transform functions or built-in transform utilities.
759
+
760
+ ### Basic Transform
761
+
762
+ ```typescript
763
+ import { Dto, Field, Transform, t } from "adorn-api";
764
+
765
+ @Dto()
766
+ export class UserDto {
767
+ @Field(t.string())
768
+ @Transform((value) => value.toUpperCase())
769
+ name!: string;
770
+
771
+ @Field(t.dateTime())
772
+ @Transform((value) => value.toISOString())
773
+ createdAt!: Date;
774
+ }
775
+ ```
776
+
777
+ ### Built-in Transforms
778
+
779
+ Adorn API includes common transform functions:
780
+
781
+ ```typescript
782
+ import { Dto, Field, Transform, Transforms, t } from "adorn-api";
783
+
784
+ @Dto()
785
+ export class UserDto {
786
+ @Field(t.string())
787
+ @Transform(Transforms.toLowerCase)
788
+ email!: string;
789
+
790
+ @Field(t.number())
791
+ @Transform(Transforms.round(2))
792
+ price!: number;
793
+
794
+ @Field(t.string())
795
+ @Transform(Transforms.mask(4)) // Mask all but last 4 characters
796
+ creditCard!: string;
797
+
798
+ @Field(t.dateTime())
799
+ @Transform(Transforms.toISOString)
800
+ birthDate!: Date;
801
+ }
802
+ ```
803
+
804
+ ### Conditional Transforms with Groups
805
+
806
+ Apply transforms only to specific serialization groups:
807
+
808
+ ```typescript
809
+ import { Dto, Field, Transform, Expose, t } from "adorn-api";
810
+
811
+ @Dto()
812
+ export class UserDto {
813
+ @Field(t.string())
814
+ name!: string;
815
+
816
+ @Field(t.string())
817
+ @Expose({ groups: ["admin"] })
818
+ @Transform((value) => Transforms.mask(2), { groups: ["admin"] })
819
+ phoneNumber!: string;
820
+
821
+ @Field(t.string())
822
+ @Expose({ groups: ["internal"] })
823
+ @Transform((value) => "[REDACTED]", { groups: ["external"] })
824
+ internalNote!: string;
825
+ }
826
+ ```
827
+
828
+ ### Custom Transform Functions
829
+
830
+ Create custom transform functions:
831
+
832
+ ```typescript
833
+ import { Dto, Field, Transform, t } from "adorn-api";
834
+
835
+ const toCurrency = (value: number, currency: string = "USD") => {
836
+ return new Intl.NumberFormat("en-US", {
837
+ style: "currency",
838
+ currency
839
+ }).format(value);
840
+ };
841
+
842
+ @Dto()
843
+ export class ProductDto {
844
+ @Field(t.string())
845
+ name!: string;
846
+
847
+ @Field(t.number())
848
+ @Transform(toCurrency)
849
+ price!: number;
850
+
851
+ @Field(t.number())
852
+ @Transform((value) => toCurrency(value, "EUR"))
853
+ priceEUR!: number;
854
+ }
855
+ ```
856
+
857
+ ### Serialization with Options
858
+
859
+ Control serialization with custom options:
860
+
861
+ ```typescript
862
+ import { serialize, createSerializer } from "adorn-api";
863
+ import { UserDto } from "./user.dtos";
864
+
865
+ const user = new UserDto();
866
+ user.name = "John Doe";
867
+ user.phoneNumber = "123-456-7890";
868
+ user.internalNote = "This is an internal note";
869
+
870
+ // Basic serialization
871
+ const basic = serialize(user);
872
+ // Output: { name: "John Doe" }
873
+
874
+ // Admin group serialization
875
+ const admin = serialize(user, { groups: ["admin"] });
876
+ // Output: { name: "John Doe", phoneNumber: "********90" }
877
+
878
+ // External group serialization
879
+ const external = serialize(user, { groups: ["external"] });
880
+ // Output: { name: "John Doe", internalNote: "[REDACTED]" }
881
+
882
+ // Create a preset serializer
883
+ const adminSerializer = createSerializer({ groups: ["admin"] });
884
+ const adminData = adminSerializer(user);
885
+ ```
886
+
887
+ ### Exclude Fields
888
+
889
+ Control which fields are excluded or exposed:
890
+
891
+ ```typescript
892
+ import { Dto, Field, Exclude, Expose, t } from "adorn-api";
893
+
894
+ @Dto()
895
+ export class UserDto {
896
+ @Field(t.string())
897
+ name!: string;
898
+
899
+ @Field(t.string())
900
+ @Exclude() // Always exclude from serialization
901
+ password!: string;
902
+
903
+ @Field(t.string())
904
+ @Expose({ name: "email_address" }) // Rename field in output
905
+ email!: string;
906
+
907
+ @Field(t.string())
908
+ @Exclude({ groups: ["public"] }) // Exclude from public group
909
+ internalComment!: string;
910
+ }
911
+ ```
912
+
871
913
  ## Examples
872
-
873
- Check out the `examples/` directory for more comprehensive examples:
874
-
875
- - `basic/` - Simple API with controllers and DTOs
876
- - `restful/` - RESTful API with complete CRUD operations
914
+
915
+ Check out the `examples/` directory for more comprehensive examples:
916
+
917
+ - `basic/` - Simple API with controllers and DTOs
918
+ - `restful/` - RESTful API with complete CRUD operations
877
919
  - `metal-orm-sqlite/` - Metal ORM integration with SQLite
878
920
  - `metal-orm-tree/` - Metal ORM tree (nested set) DTO + OpenAPI integration
879
921
  - `metal-orm-deep-filters/` - Deep relation filtering example (Alpha → Bravo → Charlie → Delta)
880
922
  - `metal-orm-sqlite-music/` - Complex relations with Metal ORM
881
- - `streaming/` - SSE and streaming responses
882
- - `openapi/` - OpenAPI documentation customization
883
- - `validation/` - Comprehensive validation examples with various schema types
884
-
885
- ## Testing
886
-
887
- Adorn API works great with testing frameworks like Vitest and SuperTest. Here's an example:
888
-
889
- ```typescript
890
- import { describe, it, expect } from "vitest";
891
- import request from "supertest";
892
- import { createApp } from "./app";
893
-
894
- describe("User API", () => {
895
- it("should get user by id", async () => {
896
- const app = await createApp();
897
-
898
- const response = await request(app)
899
- .get("/users/1")
900
- .expect(200);
901
-
902
- expect(response.body).toEqual({
903
- id: "1",
904
- name: "Ada Lovelace",
905
- nickname: "Ada"
906
- });
907
- });
908
- });
909
- ```
910
-
911
- ## License
912
-
913
- MIT
914
-
915
- ## Contributing
916
-
917
- Contributions are welcome! Please feel free to submit a Pull Request.
923
+ - `streaming/` - SSE and streaming responses
924
+ - `openapi/` - OpenAPI documentation customization
925
+ - `validation/` - Comprehensive validation examples with various schema types
926
+
927
+ ## Testing
928
+
929
+ Adorn API works great with testing frameworks like Vitest and SuperTest. Here's an example:
930
+
931
+ ```typescript
932
+ import { describe, it, expect } from "vitest";
933
+ import request from "supertest";
934
+ import { createApp } from "./app";
935
+
936
+ describe("User API", () => {
937
+ it("should get user by id", async () => {
938
+ const app = await createApp();
939
+
940
+ const response = await request(app)
941
+ .get("/users/1")
942
+ .expect(200);
943
+
944
+ expect(response.body).toEqual({
945
+ id: "1",
946
+ name: "Ada Lovelace",
947
+ nickname: "Ada"
948
+ });
949
+ });
950
+ });
951
+ ```
952
+
953
+ ## License
954
+
955
+ MIT
956
+
957
+ ## Contributing
958
+
959
+ Contributions are welcome! Please feel free to submit a Pull Request.