adorn-api 1.0.30 → 1.0.31

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 (139) hide show
  1. package/README.md +529 -643
  2. package/dist/adapter/express/controllers.d.ts +3 -2
  3. package/dist/adapter/express/controllers.js +63 -4
  4. package/dist/adapter/express/index.d.ts +6 -1
  5. package/dist/adapter/express/index.js +13 -2
  6. package/dist/adapter/express/multipart.d.ts +30 -0
  7. package/dist/adapter/express/multipart.js +194 -0
  8. package/dist/adapter/express/types.d.ts +57 -1
  9. package/dist/adapter/metal-orm/crud-dtos.d.ts +1 -1
  10. package/dist/adapter/metal-orm/crud-dtos.js +9 -5
  11. package/dist/adapter/metal-orm/field-builder.d.ts +23 -0
  12. package/dist/adapter/metal-orm/field-builder.js +50 -3
  13. package/dist/adapter/metal-orm/index.d.ts +1 -0
  14. package/dist/adapter/metal-orm/index.js +4 -1
  15. package/dist/adapter/metal-orm/types.d.ts +6 -0
  16. package/dist/core/__tests__/auth.test.d.ts +1 -0
  17. package/dist/core/__tests__/auth.test.js +688 -0
  18. package/dist/core/__tests__/file-upload.test.d.ts +1 -0
  19. package/dist/core/__tests__/file-upload.test.js +439 -0
  20. package/dist/core/__tests__/health.test.d.ts +1 -0
  21. package/dist/core/__tests__/health.test.js +137 -0
  22. package/dist/core/__tests__/logger.test.d.ts +1 -0
  23. package/dist/core/__tests__/logger.test.js +251 -0
  24. package/dist/core/__tests__/serialization.test.d.ts +1 -0
  25. package/dist/core/__tests__/serialization.test.js +483 -0
  26. package/dist/core/auth.d.ts +121 -0
  27. package/dist/core/auth.js +233 -0
  28. package/dist/core/decorators.d.ts +57 -0
  29. package/dist/core/decorators.js +77 -0
  30. package/dist/core/health.d.ts +91 -0
  31. package/dist/core/health.js +306 -0
  32. package/dist/core/lifecycle.d.ts +82 -0
  33. package/dist/core/lifecycle.js +116 -0
  34. package/dist/core/logger.d.ts +108 -0
  35. package/dist/core/logger.js +145 -0
  36. package/dist/core/metadata.d.ts +12 -0
  37. package/dist/core/openapi.js +103 -1
  38. package/dist/core/schema-builder.js +6 -0
  39. package/dist/core/schema.d.ts +19 -1
  40. package/dist/core/schema.js +10 -0
  41. package/dist/core/serialization.d.ts +104 -0
  42. package/dist/core/serialization.js +221 -0
  43. package/dist/core/streaming.d.ts +123 -0
  44. package/dist/core/streaming.js +253 -0
  45. package/dist/core/validation/index.d.ts +12 -0
  46. package/dist/core/validation/index.js +28 -0
  47. package/dist/core/validation/validate.d.ts +10 -0
  48. package/dist/core/validation/validate.js +91 -0
  49. package/dist/core/validation/validators/array-validator.d.ts +12 -0
  50. package/dist/core/validation/validators/array-validator.js +45 -0
  51. package/dist/core/validation/validators/boolean-validator.d.ts +7 -0
  52. package/dist/core/validation/validators/boolean-validator.js +14 -0
  53. package/dist/core/validation/validators/dto-validator.d.ts +10 -0
  54. package/dist/core/validation/validators/dto-validator.js +47 -0
  55. package/dist/core/validation/validators/enum-validator.d.ts +8 -0
  56. package/dist/core/validation/validators/enum-validator.js +14 -0
  57. package/dist/core/validation/validators/literal-validator.d.ts +8 -0
  58. package/dist/core/validation/validators/literal-validator.js +14 -0
  59. package/dist/core/validation/validators/null-validator.d.ts +7 -0
  60. package/dist/core/validation/validators/null-validator.js +14 -0
  61. package/dist/core/validation/validators/number-validator.d.ts +12 -0
  62. package/dist/core/validation/validators/number-validator.js +38 -0
  63. package/dist/core/validation/validators/object-validator.d.ts +13 -0
  64. package/dist/core/validation/validators/object-validator.js +39 -0
  65. package/dist/core/validation/validators/record-validator.d.ts +9 -0
  66. package/dist/core/validation/validators/record-validator.js +26 -0
  67. package/dist/core/validation/validators/string-validator.d.ts +11 -0
  68. package/dist/core/validation/validators/string-validator.js +78 -0
  69. package/dist/core/validation/validators/union-validator.d.ts +10 -0
  70. package/dist/core/validation/validators/union-validator.js +34 -0
  71. package/dist/core/validation/validators/validation-utils.d.ts +129 -0
  72. package/dist/core/validation/validators/validation-utils.js +299 -0
  73. package/dist/core/validation-errors.d.ts +73 -0
  74. package/dist/core/validation-errors.js +88 -0
  75. package/dist/core/validation.d.ts +10 -0
  76. package/dist/core/validation.js +533 -0
  77. package/dist/index.d.ts +8 -0
  78. package/dist/index.js +8 -0
  79. package/examples/streaming/app.ts +103 -0
  80. package/examples/validation/app.ts +85 -0
  81. package/package.json +1 -1
  82. package/src/adapter/express/controllers.ts +89 -9
  83. package/src/adapter/express/index.ts +13 -2
  84. package/src/adapter/express/multipart.ts +258 -0
  85. package/src/adapter/express/types.ts +61 -1
  86. package/src/adapter/metal-orm/crud-dtos.ts +47 -43
  87. package/src/adapter/metal-orm/field-builder.ts +66 -5
  88. package/src/adapter/metal-orm/index.ts +46 -41
  89. package/src/adapter/metal-orm/types.ts +53 -47
  90. package/src/core/auth.ts +315 -0
  91. package/src/core/decorators.ts +766 -645
  92. package/src/core/health.ts +235 -0
  93. package/src/core/lifecycle.ts +160 -0
  94. package/src/core/logger.ts +247 -0
  95. package/src/core/metadata.ts +130 -117
  96. package/src/core/openapi.ts +405 -282
  97. package/src/core/schema-builder.ts +293 -287
  98. package/src/core/schema.ts +425 -400
  99. package/src/core/serialization.ts +315 -0
  100. package/src/core/streaming.ts +330 -0
  101. package/src/core/validation/index.ts +12 -0
  102. package/src/core/validation/validate.ts +104 -0
  103. package/src/core/validation/validators/array-validator.ts +60 -0
  104. package/src/core/validation/validators/boolean-validator.ts +19 -0
  105. package/src/core/validation/validators/dto-validator.ts +63 -0
  106. package/src/core/validation/validators/enum-validator.ts +24 -0
  107. package/src/core/validation/validators/literal-validator.ts +24 -0
  108. package/src/core/validation/validators/null-validator.ts +19 -0
  109. package/src/core/validation/validators/number-validator.ts +50 -0
  110. package/src/core/validation/validators/object-validator.ts +51 -0
  111. package/src/core/validation/validators/record-validator.ts +34 -0
  112. package/src/core/validation/validators/string-validator.ts +104 -0
  113. package/src/core/validation/validators/union-validator.ts +43 -0
  114. package/src/core/validation/validators/validation-utils.ts +348 -0
  115. package/src/core/validation-errors.ts +111 -0
  116. package/src/index.ts +8 -0
  117. package/{src → tests}/e2e/cors.e2e.test.ts +54 -25
  118. package/tests/e2e/file-upload.e2e.test.ts +149 -0
  119. package/{src → tests}/e2e/http-error.e2e.test.ts +56 -52
  120. package/tests/e2e/metadata-loading.e2e.test.ts +771 -0
  121. package/{src → tests}/e2e/metal-crud-openapi.e2e.test.ts +1 -1
  122. package/tests/e2e/nota-versao-issue-reproduction.e2e.test.ts +324 -0
  123. package/{src → tests}/e2e/sqlite-metal-orm.e2e.test.ts +175 -174
  124. package/{src → tests}/e2e/sqlite.e2e.test.ts +127 -126
  125. package/tests/lifecycle.test.ts +316 -0
  126. package/tests/streaming.test.ts +222 -0
  127. package/tests/unit/auth.test.ts +417 -0
  128. package/{src/core/__tests__ → tests/unit}/coerce.test.ts +1 -1
  129. package/{src/core/__tests__ → tests/unit}/dto-compose.test.ts +3 -3
  130. package/tests/unit/file-upload.test.ts +269 -0
  131. package/tests/unit/health.test.ts +157 -0
  132. package/tests/unit/logger.test.ts +322 -0
  133. package/{src/adapter → tests/unit}/metal-orm.test.ts +16 -16
  134. package/{src/core/__tests__ → tests/unit}/schema-builder.test.ts +3 -3
  135. package/tests/unit/serialization.test.ts +329 -0
  136. package/tests/unit/validation.test.ts +336 -0
  137. package/tsconfig.eslint.json +7 -7
  138. package/tsconfig.json +18 -18
  139. package/vitest.config.ts +1 -1
package/README.md CHANGED
@@ -1,643 +1,529 @@
1
- # Adorn API
2
-
3
- [![npm version](https://badge.fury.io/js/adorn-api.svg)](https://www.npmjs.com/package/adorn-api)
4
-
5
- Decorator-first web framework for TypeScript with OpenAPI 3.1 schema generation.
6
-
7
- ## Features
8
-
9
- - **Decorator-based API definition** - Use TypeScript decorators to define controllers, routes, DTOs, and schemas
10
- - **OpenAPI 3.1 generation** - Automatically generate OpenAPI 3.1 specifications from your decorators
11
- - **Express integration** - Built-in Express adapter with automatic request/response handling
12
- - **Metal ORM integration** - Seamlessly integrate with Metal ORM for database operations
13
- - **Type-safe DTOs** - Create type-safe Data Transfer Objects with composition utilities
14
- - **Input coercion** - Automatic type coercion for query parameters and path parameters (safe or strict modes)
15
- - **Error handling** - Built-in HTTP error handling with customizable error DTOs
16
- - **Swagger UI** - Built-in Swagger UI documentation
17
-
18
- ## Installation
19
-
20
- ```bash
21
- npm install adorn-api express metal-orm
22
- ```
23
-
24
- ## Project Setup
25
-
26
- Create a minimal `package.json` for your project:
27
-
28
- ```json
29
- {
30
- "name": "my-api",
31
- "version": "1.0.0",
32
- "type": "module",
33
- "scripts": {
34
- "start": "node dist/index.js",
35
- "dev": "tsx src/index.ts",
36
- "build": "tsc"
37
- },
38
- "dependencies": {
39
- "adorn-api": "^1.0.25",
40
- "express": "^4.19.2"
41
- },
42
- "devDependencies": {
43
- "@types/node": "^20.11.0",
44
- "tsx": "^4.7.0",
45
- "typescript": "^5.4.5"
46
- }
47
- }
48
- ```
49
-
50
- **Dependencies explained:**
51
- - `adorn-api` - The main framework
52
- - `express` - HTTP server (required)
53
- - `tsx` - TypeScript execution for development
54
- - `typescript` - TypeScript compiler
55
-
56
- **Scripts explained:**
57
- - `npm run dev` - Run in development mode with hot reload
58
- - `npm run build` - Compile TypeScript to JavaScript
59
- - `npm start` - Run the compiled application
60
-
61
- ## Getting Started
62
-
63
- Follow these steps to set up a new project from scratch:
64
-
65
- ### 1. Create your project directory
66
-
67
- ```bash
68
- mkdir my-api
69
- cd my-api
70
- ```
71
-
72
- ### 2. Initialize npm
73
-
74
- ```bash
75
- npm init -y
76
- ```
77
-
78
- ### 3. Install dependencies
79
-
80
- ```bash
81
- npm install adorn-api express
82
- npm install -D tsx typescript @types/node
83
- ```
84
-
85
- ### 4. Create `tsconfig.json`
86
-
87
- ```json
88
- {
89
- "compilerOptions": {
90
- "target": "ES2022",
91
- "module": "NodeNext",
92
- "moduleResolution": "NodeNext",
93
- "outDir": "./dist",
94
- "rootDir": "./src",
95
- "strict": true,
96
- "esModuleInterop": true,
97
- "skipLibCheck": true,
98
- "forceConsistentCasingInFileNames": true,
99
- "useDefineForClassFields": true,
100
- "experimentalDecorators": false,
101
- "emitDecoratorMetadata": false
102
- },
103
- "include": ["src"]
104
- }
105
- ```
106
-
107
- ### 5. Create `src/index.ts`
108
-
109
- ```typescript
110
- import { Controller, Get, Dto, Field, t, createExpressApp } from "adorn-api";
111
-
112
- @Dto({ description: "User record" })
113
- class UserDto {
114
- @Field(t.uuid({ description: "User ID" }))
115
- id!: string;
116
-
117
- @Field(t.string({ minLength: 1 }))
118
- name!: string;
119
- }
120
-
121
- @Controller("/users")
122
- class UserController {
123
- @Get("/:id")
124
- @Params(UserDto)
125
- @Returns(UserDto)
126
- async getOne(ctx: RequestContext<unknown, undefined, UserDto>) {
127
- return {
128
- id: ctx.params.id,
129
- name: "Ada Lovelace"
130
- };
131
- }
132
- }
133
-
134
- const app = createExpressApp({
135
- controllers: [UserController],
136
- openApi: {
137
- info: { title: "My API", version: "1.0.0" },
138
- docs: true
139
- }
140
- });
141
-
142
- app.listen(3000, () => {
143
- console.log("Server running at http://localhost:3000");
144
- console.log("API docs at http://localhost:3000/docs");
145
- });
146
- ```
147
-
148
- ### 6. Run your application
149
-
150
- ```bash
151
- npm run dev
152
- ```
153
-
154
- Visit http://localhost:3000/docs to see your API documentation.
155
-
156
- ## Quick Start
157
-
158
- ```typescript
159
- import { Controller, Get, Dto, Field, t, createExpressApp } from "adorn-api";
160
-
161
- @Dto({ description: "User record" })
162
- class UserDto {
163
- @Field(t.uuid({ description: "User ID" }))
164
- id!: string;
165
-
166
- @Field(t.string({ minLength: 1 }))
167
- name!: string;
168
- }
169
-
170
- @Controller("/users")
171
- class UserController {
172
- @Get("/:id")
173
- @Params(UserDto)
174
- @Returns(UserDto)
175
- async getOne(ctx: RequestContext<unknown, undefined, UserDto>) {
176
- return {
177
- id: ctx.params.id,
178
- name: "Ada Lovelace"
179
- };
180
- }
181
- }
182
-
183
- const app = createExpressApp({
184
- controllers: [UserController],
185
- openApi: {
186
- info: { title: "My API", version: "1.0.0" },
187
- docs: true
188
- }
189
- });
190
-
191
- app.listen(3000);
192
- ```
193
-
194
- Visit http://localhost:3000/docs to see your API documentation.
195
-
196
- ## Examples
197
-
198
- The repository includes several examples demonstrating different features:
199
-
200
- - **basic** - Simple controller and DTO usage
201
- - **restful** - RESTful API with full CRUD operations
202
- - **openapi** - OpenAPI documentation setup
203
- - **metal-orm-sqlite** - Metal ORM integration with SQLite
204
- - **metal-orm-sqlite-music** - Complex Metal ORM example with relationships
205
-
206
- Run an example:
207
- ```bash
208
- npm run example basic
209
- ```
210
-
211
- ## Core Concepts
212
-
213
- ### DTOs (Data Transfer Objects)
214
-
215
- DTOs define the shape of your API data:
216
-
217
- ```typescript
218
- @Dto({ description: "User data" })
219
- class UserDto {
220
- @Field(t.uuid())
221
- id!: string;
222
-
223
- @Field(t.string({ minLength: 1 }))
224
- name!: string;
225
-
226
- @Field(t.optional(t.string()))
227
- nickname?: string;
228
- }
229
- ```
230
-
231
- ### Controllers
232
-
233
- Controllers group related routes:
234
-
235
- ```typescript
236
- @Controller("/users")
237
- class UserController {
238
- @Get("/")
239
- async list() {
240
- return [{ id: "1", name: "User 1" }];
241
- }
242
-
243
- @Post("/")
244
- @Body(CreateUserDto)
245
- async create(ctx: RequestContext<CreateUserDto>) {
246
- return { id: "new-id", ...ctx.body };
247
- }
248
- }
249
- ```
250
-
251
- ### Request Context
252
-
253
- Route handlers receive a typed `RequestContext` with:
254
- - `req` - Express request
255
- - `res` - Express response
256
- - `body` - Parsed request body
257
- - `query` - Parsed query parameters
258
- - `params` - Parsed path parameters
259
- - `headers` - Request headers
260
-
261
- ## Decorators
262
-
263
- ### Controller Decorators
264
-
265
- - `@Controller(pathOrOptions)` - Define a controller with base path and tags
266
-
267
- ### HTTP Method Decorators
268
-
269
- - `@Get(path)` - GET route
270
- - `@Post(path)` - POST route
271
- - `@Put(path)` - PUT route
272
- - `@Patch(path)` - PATCH route
273
- - `@Delete(path)` - DELETE route
274
-
275
- ### Input Decorators
276
-
277
- - `@Body(schema, options)` - Request body schema
278
- - `@Query(schema, options)` - Query parameters schema
279
- - `@Params(schema, options)` - Path parameters schema
280
- - `@Headers(schema, options)` - Request headers schema
281
-
282
- ### Response Decorators
283
-
284
- - `@Returns(schemaOrOptions, options)` - Define response schema
285
- - `@ReturnsError(schemaOrOptions, options)` - Define error response
286
- - `@Errors(schema, responses)` - Define multiple error responses
287
- - `@Doc(options)` - Add route documentation
288
-
289
- ### DTO Decorators
290
-
291
- - `@Dto(options)` - Define a DTO class
292
- - `@Field(schemaOrOptions)` - Define a field in a DTO
293
-
294
- ### DTO Composition
295
-
296
- - `@PickDto(dto, keys, options)` - Create DTO with selected fields
297
- - `@OmitDto(dto, keys, options)` - Create DTO excluding fields
298
- - `@PartialDto(dto, options)` - Create DTO with all fields optional
299
- - `@MergeDto(dtos, options)` - Create DTO by merging multiple DTOs
300
-
301
- ## Schema Builder
302
-
303
- The `t` object provides type-safe schema definitions:
304
-
305
- ```typescript
306
- t.string({ minLength: 1, maxLength: 100, pattern: "^[a-z]+$" })
307
- t.uuid({ description: "Unique identifier" })
308
- t.dateTime()
309
- t.number({ minimum: 0, maximum: 100, exclusiveMaximum: true })
310
- t.integer({ multipleOf: 5 })
311
- t.boolean()
312
- t.array(t.string(), { minItems: 1, maxItems: 10 })
313
- t.object({ name: t.string(), age: t.integer() })
314
- t.record(t.string())
315
- t.enum(["active", "inactive"])
316
- t.literal("admin")
317
- t.union([t.string(), t.integer()])
318
- t.ref(SomeDto)
319
- t.any()
320
- t.null()
321
- t.optional(schema)
322
- t.nullable(schema)
323
- ```
324
-
325
- ## OpenAPI Documentation
326
-
327
- Enable OpenAPI documentation:
328
-
329
- ```typescript
330
- createExpressApp({
331
- controllers: [MyController],
332
- openApi: {
333
- info: {
334
- title: "My API",
335
- version: "1.0.0",
336
- description: "API description"
337
- },
338
- servers: [{ url: "https://api.example.com", description: "Production" }],
339
- path: "/openapi.json", // JSON spec path (default: /openapi.json)
340
- docs: true, // Enable Swagger UI (default: /docs)
341
- docs: {
342
- path: "/docs", // Swagger UI path
343
- title: "API Docs",
344
- swaggerUiUrl: "https://unpkg.com/swagger-ui-dist@5"
345
- }
346
- }
347
- });
348
- ```
349
-
350
- ## Metal ORM Integration
351
-
352
- ### CRUD DTOs
353
-
354
- Automatically create CRUD DTOs from Metal entities:
355
-
356
- ```typescript
357
- import { User } from "./entities";
358
- import { createMetalCrudDtoClasses, createMetalDtoOverrides } from "adorn-api";
359
-
360
- const overrides = createMetalDtoOverrides(User, {
361
- overrides: {
362
- email: t.nullable(t.string({ format: "email" }))
363
- }
364
- });
365
-
366
- const crud = createMetalCrudDtoClasses(User, {
367
- overrides,
368
- response: { description: "User response" },
369
- mutationExclude: ["id", "createdAt"]
370
- });
371
-
372
- const { UserDto, CreateUserDto, UpdateUserDto, UserParamsDto } = crud;
373
- ```
374
-
375
- ### Pagination
376
-
377
- ```typescript
378
- import { createPagedQueryDtoClass, createPagedResponseDtoClass, parsePagination } from "adorn-api";
379
-
380
- const PagedQueryDto = createPagedQueryDtoClass({
381
- name: "PagedQueryDto",
382
- defaultPageSize: 20,
383
- maxPageSize: 100
384
- });
385
-
386
- const PagedResponseDto = createPagedResponseDtoClass({
387
- name: "PagedResponseDto",
388
- itemDto: UserDto
389
- });
390
-
391
- // In controller:
392
- @Get("/")
393
- @Query(PagedQueryDto)
394
- @Returns(PagedResponseDto)
395
- async list(ctx: RequestContext<unknown, PagedQueryDto>) {
396
- const pagination = parsePagination(ctx.query);
397
- // Use pagination for queries...
398
- }
399
- ```
400
-
401
- ### Filtering
402
-
403
- ```typescript
404
- import { createPagedFilterQueryDtoClass, createFilterMappings, parseFilter } from "adorn-api";
405
-
406
- const UserQueryDto = createPagedFilterQueryDtoClass({
407
- name: "UserQueryDto",
408
- filters: {
409
- nameContains: { schema: t.string(), operator: "contains" },
410
- ageGte: { schema: t.integer(), operator: "gte" },
411
- active: { schema: t.boolean() }
412
- }
413
- });
414
-
415
- // In controller:
416
- const filterMappings = createFilterMappings(User, {
417
- nameContains: "name",
418
- ageGte: "age",
419
- active: "active"
420
- });
421
- const filters = parseFilter(ctx.query, filterMappings);
422
- ```
423
-
424
- ## Error Handling
425
-
426
- ### HttpError
427
-
428
- ```typescript
429
- import { HttpError } from "adorn-api";
430
-
431
- // Simple error
432
- throw new HttpError(404, "User not found");
433
-
434
- // With body
435
- throw new HttpError(400, "Validation failed", {
436
- errors: [{ field: "email", message: "Invalid email" }]
437
- });
438
-
439
- // With headers
440
- throw new HttpError(401, "Unauthorized", undefined, {
441
- "WWW-Authenticate": 'Bearer realm="api"'
442
- });
443
-
444
- // With options object
445
- throw new HttpError({
446
- status: 500,
447
- message: "Internal error",
448
- body: { code: "INTERNAL_ERROR" },
449
- cause: originalError
450
- });
451
- ```
452
-
453
- ### Error DTOs
454
-
455
- ```typescript
456
- import { createErrorDtoClass, SimpleErrorDto, StandardErrorDto, Errors } from "adorn-api";
457
-
458
- const ValidationErrorDto = createErrorDtoClass({
459
- name: "ValidationErrorDto",
460
- schema: t.object({
461
- field: t.string(),
462
- message: t.string()
463
- })
464
- });
465
-
466
- // In controller:
467
- @Get("/:id")
468
- @Params(UserParamsDto)
469
- @Returns(UserDto)
470
- @Errors(SimpleErrorDto, [
471
- { status: 400, description: "Invalid user ID" },
472
- { status: 404, description: "User not found" }
473
- ])
474
- async getOne(ctx: RequestContext<unknown, UserParamsDto>) {
475
- // ...
476
- }
477
- ```
478
-
479
- ## Input Coercion
480
-
481
- Configure input coercion for query and path parameters:
482
-
483
- ```typescript
484
- createExpressApp({
485
- controllers: [MyController],
486
- inputCoercion: "safe" // "safe" | "strict" | false
487
- });
488
- ```
489
-
490
- - **safe**: Coerces values, ignores failures
491
- - **strict**: Coerces values, throws on failures
492
- - **false**: Disables coercion
493
-
494
- ## Development
495
-
496
- ```bash
497
- # Build
498
- npm run build
499
-
500
- # Run tests
501
- npm test
502
-
503
- # Run tests in watch mode
504
- npm run test:watch
505
-
506
- # Run linting
507
- npm run lint
508
-
509
- # Run examples
510
- npm run example basic
511
- ```
512
-
513
- ## Common Pitfalls
514
-
515
- ⚠️ **Important:** Adorn API uses standard ECMAScript decorators (Stage 3), NOT legacy TypeScript decorators.
516
-
517
- ### ❌ DO NOT use these:
518
-
519
- ```json
520
- {
521
- "compilerOptions": {
522
- "experimentalDecorators": true, // ❌ WRONG - must be false
523
- "emitDecoratorMetadata": true // ❌ WRONG - must be false
524
- }
525
- }
526
- ```
527
-
528
- ```bash
529
- npm install reflect-metadata # WRONG - not needed
530
- tsx -r reflect-metadata src/index.ts # ❌ WRONG - remove -r flag
531
- ```
532
-
533
- ### ✅ DO use these:
534
-
535
- ```json
536
- {
537
- "compilerOptions": {
538
- "experimentalDecorators": false, // ✅ CORRECT
539
- "emitDecoratorMetadata": false // ✅ CORRECT
540
- }
541
- }
542
- ```
543
-
544
- ```bash
545
- tsx src/index.ts # ✅ CORRECT - no reflect-metadata
546
- ```
547
-
548
- ### Why?
549
-
550
- - **Standard ECMAScript decorators** (Stage 3) are the modern, standardized approach
551
- - **Legacy TypeScript decorators** (`experimentalDecorators`) are deprecated
552
- - `reflect-metadata` is only needed for legacy decorators
553
- - TypeScript 5.0+ fully supports standard decorators
554
-
555
- ### Troubleshooting
556
-
557
- If you see errors like:
558
- - `Decorators are not enabled` → Check `experimentalDecorators` is `false`
559
- - `Cannot find module 'reflect-metadata'` → Remove `reflect-metadata` from dependencies
560
- - `Decorator metadata not available` → This is expected with standard decorators
561
-
562
- ## TypeScript Configuration
563
-
564
- Ensure your `tsconfig.json` has:
565
-
566
- ```json
567
- {
568
- "compilerOptions": {
569
- "target": "ES2022",
570
- "module": "NodeNext",
571
- "moduleResolution": "NodeNext",
572
- "outDir": "./dist",
573
- "rootDir": "./src",
574
- "strict": true,
575
- "esModuleInterop": true,
576
- "skipLibCheck": true,
577
- "forceConsistentCasingInFileNames": true,
578
- "useDefineForClassFields": true,
579
- "experimentalDecorators": false, // ⚠️ MUST be false
580
- "emitDecoratorMetadata": false // ⚠️ MUST be false
581
- },
582
- "include": ["src"]
583
- }
584
- ```
585
-
586
- **Critical settings:**
587
- - `experimentalDecorators: false` - Use standard ECMAScript decorators
588
- - `emitDecoratorMetadata: false` - Not needed with standard decorators
589
- - `useDefineForClassFields: true` - Required for standard decorators
590
-
591
- Adorn API uses standard ECMAScript decorators (Stage 3).
592
-
593
- ## Quick Reference
594
-
595
- ### Setup Checklist
596
-
597
- - [ ] Initialize project: `npm init -y`
598
- - [ ] Install dependencies: `npm install adorn-api express`
599
- - [ ] Install dev dependencies: `npm install -D tsx typescript @types/node`
600
- - [ ] Create `tsconfig.json` with correct decorator settings
601
- - [ ] Create `src/index.ts` with your controllers
602
- - [ ] Run: `npm run dev`
603
-
604
- ### Common Commands
605
-
606
- ```bash
607
- # Development
608
- npm run dev # Run with hot reload
609
- npm run build # Compile TypeScript
610
- npm start # Run compiled app
611
-
612
- # Testing
613
- npm test # Run tests
614
- npm run test:watch # Run tests in watch mode
615
-
616
- # Code Quality
617
- npm run lint # Run ESLint
618
- ```
619
-
620
- ### Project Structure
621
-
622
- ```
623
- my-api/
624
- ├── src/
625
- │ └── index.ts # Main entry point
626
- ├── dist/ # Compiled JavaScript (generated)
627
- ├── node_modules/
628
- ├── package.json
629
- └── tsconfig.json
630
- ```
631
-
632
- ### Troubleshooting
633
-
634
- | Issue | Solution |
635
- |-------|----------|
636
- | Decorators not working | Check `experimentalDecorators: false` in tsconfig.json |
637
- | reflect-metadata errors | Remove `reflect-metadata` from package.json and scripts |
638
- | Module not found | Ensure `moduleResolution: "NodeNext"` in tsconfig.json |
639
- | Type errors | Run `npm run build` to see full TypeScript errors |
640
-
641
- ## License
642
-
643
- Check the package for license information.
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
+ - 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs
13
+ - 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
14
+ - 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
15
+ - 🔒 **Error Handling**: Structured error responses with error DTO support
16
+ - 💾 **File Uploads**: Easy handling of file uploads with multipart form data
17
+ - 🌐 **CORS Support**: Built-in CORS configuration
18
+ - 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install adorn-api
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ### 1. Define DTOs
29
+
30
+ ```typescript
31
+ // user.dtos.ts
32
+ import { Dto, Field, OmitDto, PickDto, t } from "adorn-api";
33
+
34
+ @Dto({ description: "User record returned by the API." })
35
+ export class UserDto {
36
+ @Field(t.uuid({ description: "User identifier." }))
37
+ id!: string;
38
+
39
+ @Field(t.string({ minLength: 1 }))
40
+ name!: string;
41
+
42
+ @Field(t.optional(t.string()))
43
+ nickname?: string;
44
+ }
45
+
46
+ @OmitDto(UserDto, ["id"])
47
+ export class CreateUserDto {}
48
+
49
+ @PickDto(UserDto, ["id"])
50
+ export class UserParamsDto {}
51
+ ```
52
+
53
+ ### 2. Create a Controller
54
+
55
+ ```typescript
56
+ // user.controller.ts
57
+ import {
58
+ Body,
59
+ Controller,
60
+ Get,
61
+ Params,
62
+ Post,
63
+ Returns,
64
+ type RequestContext
65
+ } from "adorn-api";
66
+ import { CreateUserDto, UserDto, UserParamsDto } from "./user.dtos";
67
+
68
+ @Controller("/users")
69
+ export class UserController {
70
+ @Get("/:id")
71
+ @Params(UserParamsDto)
72
+ @Returns(UserDto)
73
+ async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
74
+ return {
75
+ id: ctx.params.id,
76
+ name: "Ada Lovelace",
77
+ nickname: "Ada"
78
+ };
79
+ }
80
+
81
+ @Post("/")
82
+ @Body(CreateUserDto)
83
+ @Returns({ status: 201, schema: UserDto, description: "Created" })
84
+ async create(ctx: RequestContext<CreateUserDto>) {
85
+ return {
86
+ id: "3f0f4d0f-1cb1-4cf1-9c32-3d4bce1b3f36",
87
+ name: ctx.body.name,
88
+ nickname: ctx.body.nickname
89
+ };
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### 3. Bootstrap the Application
95
+
96
+ ```typescript
97
+ // app.ts
98
+ import { createExpressApp } from "adorn-api";
99
+ import { UserController } from "./user.controller";
100
+
101
+ export async function createApp() {
102
+ return createExpressApp({
103
+ controllers: [UserController],
104
+ openApi: {
105
+ info: {
106
+ title: "Adorn API",
107
+ version: "1.0.0"
108
+ },
109
+ docs: true
110
+ }
111
+ });
112
+ }
113
+
114
+ // index.ts
115
+ import { createApp } from "./app";
116
+
117
+ async function start() {
118
+ const app = await createApp();
119
+ const PORT = 3000;
120
+
121
+ app.listen(PORT, () => {
122
+ console.log(`Server running at http://localhost:${PORT}`);
123
+ console.log(`OpenAPI documentation: http://localhost:${PORT}/openapi.json`);
124
+ });
125
+ }
126
+
127
+ start().catch(error => {
128
+ console.error("Failed to start server:", error);
129
+ process.exit(1);
130
+ });
131
+ ```
132
+
133
+ ## Core Concepts
134
+
135
+ ### Controllers
136
+
137
+ Controllers are classes decorated with `@Controller()` that group related API endpoints. Each controller has a base path and contains route handlers.
138
+
139
+ ```typescript
140
+ @Controller("/api/v1/users")
141
+ export class UserController {
142
+ // Routes go here
143
+ }
144
+ ```
145
+
146
+ ### Routes
147
+
148
+ Routes are methods decorated with HTTP verb decorators like `@Get()`, `@Post()`, `@Put()`, `@Patch()`, or `@Delete()`.
149
+
150
+ ```typescript
151
+ @Get("/:id")
152
+ @Params(UserParamsDto)
153
+ @Returns(UserDto)
154
+ async getOne(ctx: RequestContext) {
155
+ // Route handler logic
156
+ }
157
+ ```
158
+
159
+ ### DTOs (Data Transfer Objects)
160
+
161
+ DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
162
+
163
+ ```typescript
164
+ @Dto({ description: "User data" })
165
+ export class UserDto {
166
+ @Field(t.uuid({ description: "Unique identifier" }))
167
+ id!: string;
168
+
169
+ @Field(t.string({ minLength: 2, maxLength: 100 }))
170
+ name!: string;
171
+ }
172
+ ```
173
+
174
+ ### Request Context
175
+
176
+ Each route handler receives a `RequestContext` object that provides access to:
177
+ - `ctx.body` - The request body (validated and typed)
178
+ - `ctx.params` - Route parameters
179
+ - `ctx.query` - Query parameters
180
+ - `ctx.headers` - Request headers
181
+ - `ctx.req` - The raw Express request
182
+ - `ctx.res` - The raw Express response
183
+ - `ctx.sse` - SSE emitter (for SSE routes)
184
+ - `ctx.stream` - Streaming writer (for streaming routes)
185
+
186
+ ## Advanced Features
187
+
188
+ ### Server-Sent Events (SSE)
189
+
190
+ ```typescript
191
+ import { Controller, Get, Sse } from "adorn-api";
192
+
193
+ @Controller("/events")
194
+ class EventsController {
195
+ @Get("/")
196
+ @Sse({ description: "Real-time events stream" })
197
+ async streamEvents(ctx: any) {
198
+ const emitter = ctx.sse;
199
+
200
+ let count = 0;
201
+ const interval = setInterval(() => {
202
+ count++;
203
+ emitter.emit("message", {
204
+ id: count,
205
+ timestamp: new Date().toISOString(),
206
+ message: `Event ${count}`
207
+ });
208
+
209
+ if (count >= 5) {
210
+ clearInterval(interval);
211
+ emitter.close();
212
+ }
213
+ }, 1000);
214
+
215
+ ctx.req.on("close", () => {
216
+ clearInterval(interval);
217
+ emitter.close();
218
+ });
219
+ }
220
+ }
221
+ ```
222
+
223
+ ### Streaming Responses
224
+
225
+ ```typescript
226
+ import { Controller, Get, Streaming } from "adorn-api";
227
+
228
+ @Controller("/streaming")
229
+ class StreamingController {
230
+ @Get("/")
231
+ @Streaming({ contentType: "text/plain" })
232
+ async streamText(ctx: any) {
233
+ const writer = ctx.stream;
234
+ const data = ["First line", "Second line", "Third line"];
235
+
236
+ for (let i = 0; i < data.length; i++) {
237
+ writer.writeLine(data[i]);
238
+ await new Promise(resolve => setTimeout(resolve, 500));
239
+ }
240
+
241
+ writer.close();
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### File Uploads
247
+
248
+ ```typescript
249
+ import { Controller, Post, UploadedFile, Returns, t } from "adorn-api";
250
+
251
+ @Controller("/uploads")
252
+ class UploadController {
253
+ @Post("/")
254
+ @UploadedFile("file", t.file({ accept: ["image/*"], maxSize: 5 * 1024 * 1024 }))
255
+ @Returns({ status: 200, schema: t.string() })
256
+ async uploadFile(ctx: any) {
257
+ const file = ctx.files?.file[0];
258
+ return `File uploaded: ${file.originalname}`;
259
+ }
260
+ }
261
+ ```
262
+
263
+ ## Metal ORM Integration
264
+
265
+ Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
266
+
267
+ ### 1. Define Entities
268
+
269
+ ```typescript
270
+ // user.entity.ts
271
+ import { Entity, PrimaryKey, Property } from "metal-orm";
272
+
273
+ @Entity("users")
274
+ export class User {
275
+ @PrimaryKey()
276
+ id!: number;
277
+
278
+ @Property()
279
+ name!: string;
280
+
281
+ @Property({ nullable: true })
282
+ nickname?: string;
283
+ }
284
+ ```
285
+
286
+ ### 2. Generate CRUD DTOs
287
+
288
+ ```typescript
289
+ // user.dtos.ts
290
+ import { createMetalCrudDtoClasses } from "adorn-api";
291
+ import { User } from "./user.entity";
292
+
293
+ export const {
294
+ GetUserDto,
295
+ CreateUserDto,
296
+ UpdateUserDto,
297
+ ReplaceUserDto,
298
+ UserQueryDto,
299
+ UserPagedResponseDto
300
+ } = createMetalCrudDtoClasses(User);
301
+ ```
302
+
303
+ ### 3. Create a CRUD Controller
304
+
305
+ ```typescript
306
+ // user.controller.ts
307
+ import {
308
+ Controller,
309
+ Get,
310
+ Post,
311
+ Put,
312
+ Patch,
313
+ Delete,
314
+ Params,
315
+ Body,
316
+ Query,
317
+ Returns,
318
+ parsePagination,
319
+ type RequestContext
320
+ } from "adorn-api";
321
+ import { applyFilter, toPagedResponse } from "metal-orm";
322
+ import { createSession } from "./db";
323
+ import { User } from "./user.entity";
324
+ import {
325
+ GetUserDto,
326
+ CreateUserDto,
327
+ UpdateUserDto,
328
+ ReplaceUserDto,
329
+ UserQueryDto,
330
+ UserPagedResponseDto
331
+ } from "./user.dtos";
332
+
333
+ @Controller("/users")
334
+ export class UserController {
335
+ @Get("/")
336
+ @Query(UserQueryDto)
337
+ @Returns(UserPagedResponseDto)
338
+ async list(ctx: RequestContext<unknown, UserQueryDto>) {
339
+ const { page, pageSize } = parsePagination(ctx.query);
340
+ const session = createSession();
341
+
342
+ try {
343
+ const query = applyFilter(
344
+ User.select().orderBy(User.id, "ASC"),
345
+ User,
346
+ ctx.query
347
+ );
348
+
349
+ const paged = await query.executePaged(session, { page, pageSize });
350
+ return toPagedResponse(paged);
351
+ } finally {
352
+ await session.dispose();
353
+ }
354
+ }
355
+
356
+ @Get("/:id")
357
+ @Params({ id: t.integer() })
358
+ @Returns(GetUserDto)
359
+ async getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
360
+ const session = createSession();
361
+
362
+ try {
363
+ const user = await session.find(User, parseInt(ctx.params.id));
364
+ return user;
365
+ } finally {
366
+ await session.dispose();
367
+ }
368
+ }
369
+
370
+ // Other CRUD operations...
371
+ }
372
+ ```
373
+
374
+ ## Configuration
375
+
376
+ ### Express App Options
377
+
378
+ ```typescript
379
+ createExpressApp({
380
+ // Required
381
+ controllers: [UserController],
382
+
383
+ // Optional
384
+ cors: true, // Enable CORS with default options or configure
385
+ jsonBody: true, // Parse JSON bodies (default: true)
386
+ inputCoercion: "safe", // Input coercion mode ("safe" or "strict")
387
+ multipart: { // File upload configuration
388
+ dest: "./uploads",
389
+ limits: { fileSize: 50 * 1024 * 1024 }
390
+ },
391
+ openApi: {
392
+ info: {
393
+ title: "My API",
394
+ version: "1.0.0",
395
+ description: "API documentation"
396
+ },
397
+ path: "/openapi.json", // OpenAPI schema endpoint
398
+ docs: true // Serve Swagger UI
399
+ }
400
+ });
401
+ ```
402
+
403
+ ## Schema Types
404
+
405
+ The `t` object provides a rich set of schema types:
406
+
407
+ - Primitives: `t.string()`, `t.number()`, `t.integer()`, `t.boolean()`
408
+ - Formats: `t.uuid()`, `t.dateTime()`
409
+ - Complex: `t.array()`, `t.object()`, `t.record()`
410
+ - Combinators: `t.union()`, `t.enum()`, `t.literal()`
411
+ - Special: `t.ref()`, `t.any()`, `t.null()`, `t.file()`
412
+ - Modifiers: `t.optional()`, `t.nullable()`
413
+
414
+ ## DTO Composition
415
+
416
+ Reuse and compose DTOs with these decorators:
417
+
418
+ ```typescript
419
+ // Pick specific fields from an existing DTO
420
+ @PickDto(UserDto, ["id", "name"])
421
+ export class UserSummaryDto {}
422
+
423
+ // Omit specific fields from an existing DTO
424
+ @OmitDto(UserDto, ["password"])
425
+ export class PublicUserDto {}
426
+
427
+ // Make all fields optional
428
+ @PartialDto(UserDto)
429
+ export class UpdateUserDto {}
430
+
431
+ // Merge multiple DTOs
432
+ @MergeDto([UserDto, AddressDto])
433
+ export class UserWithAddressDto {}
434
+ ```
435
+
436
+ ## Error Handling
437
+
438
+ Define structured error responses:
439
+
440
+ ```typescript
441
+ import { Controller, Get, ReturnsError, t } from "adorn-api";
442
+
443
+ @Controller("/")
444
+ class ErrorController {
445
+ @Get("/error")
446
+ @ReturnsError({
447
+ status: 404,
448
+ schema: t.object({
449
+ code: t.string(),
450
+ message: t.string(),
451
+ details: t.optional(t.record(t.any()))
452
+ }),
453
+ description: "Resource not found"
454
+ })
455
+ async notFound() {
456
+ throw new HttpError(404, "Resource not found", { code: "NOT_FOUND" });
457
+ }
458
+ }
459
+ ```
460
+
461
+ ## Lifecycle Hooks
462
+
463
+ ```typescript
464
+ import {
465
+ OnApplicationBootstrap,
466
+ OnShutdown
467
+ } from "adorn-api";
468
+
469
+ class DatabaseService implements OnApplicationBootstrap, OnShutdown {
470
+ async onApplicationBootstrap() {
471
+ console.log("Connecting to database...");
472
+ // Initialize database connection
473
+ }
474
+
475
+ async onShutdown(signal?: string) {
476
+ console.log(`Shutting down (${signal})...`);
477
+ // Cleanup resources
478
+ }
479
+ }
480
+
481
+ // Register the service
482
+ import { lifecycleRegistry } from "adorn-api";
483
+ lifecycleRegistry.register(new DatabaseService());
484
+ ```
485
+
486
+ ## Examples
487
+
488
+ Check out the `examples/` directory for more comprehensive examples:
489
+
490
+ - `basic/` - Simple API with controllers and DTOs
491
+ - `restful/` - RESTful API with complete CRUD operations
492
+ - `metal-orm-sqlite/` - Metal ORM integration with SQLite
493
+ - `metal-orm-sqlite-music/` - Complex relations with Metal ORM
494
+ - `streaming/` - SSE and streaming responses
495
+ - `openapi/` - OpenAPI documentation customization
496
+
497
+ ## Testing
498
+
499
+ Adorn API works great with testing frameworks like Vitest and SuperTest. Here's an example:
500
+
501
+ ```typescript
502
+ import { describe, it, expect } from "vitest";
503
+ import request from "supertest";
504
+ import { createApp } from "./app";
505
+
506
+ describe("User API", () => {
507
+ it("should get user by id", async () => {
508
+ const app = await createApp();
509
+
510
+ const response = await request(app)
511
+ .get("/users/1")
512
+ .expect(200);
513
+
514
+ expect(response.body).toEqual({
515
+ id: "1",
516
+ name: "Ada Lovelace",
517
+ nickname: "Ada"
518
+ });
519
+ });
520
+ });
521
+ ```
522
+
523
+ ## License
524
+
525
+ MIT
526
+
527
+ ## Contributing
528
+
529
+ Contributions are welcome! Please feel free to submit a Pull Request.