@tahminator/sapling 2.0.5 → 2.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
@@ -19,9 +19,13 @@ A lightweight Express.js dependency injection & route abstraction library.
19
19
  * [Responses](#responses)
20
20
  * [Error Handling](#error-handling)
21
21
  * [Middleware](#middleware)
22
- * [Request Validation](#request-validation)
22
+ * [Request/Response Validation](#requestresponse-validation)
23
23
  * [Redirects](#redirects)
24
24
  * [Dependency Injection](#dependency-injection)
25
+ * [OpenAPI Support](#openapi-support)
26
+ + [Automatic Spec Generation](#automatic-spec-generation)
27
+ + [Adding Documentation with Decorators](#adding-documentation-with-decorators)
28
+ + [Customizing Paths](#customizing-paths)
25
29
  * [Custom Serialization](#custom-serialization)
26
30
  - [Advanced Setup](#advanced-setup)
27
31
  * [Automatically import controllers](#automatically-import-controllers)
@@ -107,6 +111,16 @@ middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
107
111
  const controllers: Class<any>[] = [HelloController, UserController];
108
112
  controllers.map(Sapling.resolve).forEach((r) => app.use(r));
109
113
 
114
+ // @MiddlewareClass handling custom errors should be handled after everything else.
115
+ // Sapling includes basic `DefaultResponseStatusErrorMiddleware` & `BaseErrorMiddleware`
116
+ // but you may write your own, even to replace these defaults!
117
+ const errorMiddlewares: Class<any>[] = [
118
+ DefaultParserErrorMiddleware,
119
+ DefaultResponseStatusErrorMiddleware,
120
+ DefaultBaseErrorMiddleware,
121
+ ];
122
+ errorMiddlewares.map(Sapling.resolve).forEach((r) => app.use(r));
123
+
110
124
  app.listen(3000);
111
125
  ```
112
126
 
@@ -142,12 +156,9 @@ Sapling supports the usual suspects:
142
156
  - `@PUT(path?)`
143
157
  - `@DELETE(path?)`
144
158
  - `@PATCH(path?)`
145
- - `@Middleware(path?)` - for middleware
146
- - `@RequestBody(schema)` - validate & parse the request body
147
- - `@RequestParam(schema)` - validate & parse route params
148
- - `@RequestQuery(schema)` - validate & parse the query string
159
+ - `@Middleware(path?)` (alias of `@USE(path?)`) - for middleware
149
160
 
150
- Path defaults to `"/"` if you don't pass one. The request schema decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validator (e.g. Zod, Valibot, ArkType).
161
+ Path defaults to `"/"` if you don't pass one.
151
162
 
152
163
  ### Responses
153
164
 
@@ -218,6 +229,7 @@ controllers.map(Sapling.resolve).forEach((r) => app.use(r));
218
229
 
219
230
  // error middlewares last
220
231
  const errorMiddlewares: Class<any>[] = [
232
+ DefaultParserErrorMiddleware,
221
233
  DefaultResponseStatusErrorMiddleware,
222
234
  DefaultBaseErrorMiddleware,
223
235
  ];
@@ -328,11 +340,18 @@ app.use(Sapling.resolve(RequestTimerMiddleware));
328
340
  app.use(Sapling.resolve(UserController));
329
341
  ```
330
342
 
331
- ### Request Validation
343
+ ### Request/Response Validation
344
+
345
+ Validate and transform request request & response inputs / outputs.
346
+
347
+ - `@RequestBody(schema)` - validate & parse the request body
348
+ - `@RequestParam(schema)` - validate & parse route params
349
+ - `@RequestQuery(schema)` - validate & parse the query string
350
+ - `@ResponseBody(schema)` - validate & parse the response body
332
351
 
333
- Validate and transform request bodies, route params, and query strings at the controller level using `@RequestBody`, `@RequestParam`, and `@RequestQuery`. These decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validator (Zod, Valibot, ArkType, etc.).
352
+ These decorators accept any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible validation library (Zod, Valibot, ArkType, etc.).
334
353
 
335
- If validation fails, a `ParserError` is thrown, which Express handles as a `400 Bad Request` by default:
354
+ If validation fails, a `ParserError` is thrown. You may register `DefaultParserErrorMiddleware` for default error handling or write your own:
336
355
 
337
356
  ```typescript
338
357
  import { z } from "zod";
@@ -340,6 +359,7 @@ import { z } from "zod";
340
359
  const CreateUserSchema = z.object({ name: z.string(), age: z.number() });
341
360
  const UserParamsSchema = z.object({ id: z.string() });
342
361
  const ListUsersQuerySchema = z.object({ page: z.coerce.number() });
362
+ const ListUserIdsResponseSchema = z.array(z.object({ id: z.int() }));
343
363
 
344
364
  @Controller({ prefix: "/users" })
345
365
  class UserController {
@@ -366,14 +386,15 @@ class UserController {
366
386
  }
367
387
 
368
388
  @RequestQuery(ListUsersQuerySchema)
389
+ @ResponseBody(ListUserIdsResponseSchema) // since we have this annotation, it will a) strip all extra fields & b) throw if we are missing any expected fields.
369
390
  @GET()
370
- listUsers(request: Request): ResponseEntity<z.infer<typeof ListUsersQuerySchema>> {
391
+ listUserIds(request: Request): ResponseEntity<z.infer<typeof ListUserIdsResponseSchema>> {
371
392
  // request.query has been fully validated and rewritten. you can safely assert the type!
372
393
  const query = request.query as unknown as z.infer<typeof ListUsersQuerySchema>;
373
394
 
374
- const users = this.database.user.findAll({ page: query.page });
395
+ const userIds = this.database.user.findAll({ page: query.page }).map(u => u.id);
375
396
 
376
- return ResponseEntity.ok().body(users);
397
+ return ResponseEntity.ok().body(userIds);
377
398
  }
378
399
  }
379
400
  ```
@@ -432,6 +453,134 @@ class UserRepository {
432
453
  }
433
454
  ```
434
455
 
456
+ ### OpenAPI Support
457
+
458
+ Sapling automatically generates OpenAPI 3.0 specifications from your controllers and serves them with Swagger UI.
459
+
460
+ #### Automatic Spec Generation
461
+
462
+ Configure your API metadata and serve the OpenAPI spec:
463
+
464
+ ```typescript
465
+ import {
466
+ Sapling,
467
+ DefaultOpenApiMiddleware,
468
+ DefaultSwaggerMiddleware,
469
+ } from "@tahminator/sapling";
470
+
471
+ // Configure API metadata
472
+ Sapling.Extras.swaggerAndOpenApi.setMetadata({
473
+ title: "My API",
474
+ version: "1.0.0",
475
+ description: "API documentation for my application",
476
+ });
477
+
478
+ // Serve OpenAPI spec at /openapi.json and Swagger UI at /swagger.html
479
+ // modify default routes with `Sapling.Extras.swaggerAndOpenApi`
480
+ const middlewares = [
481
+ DefaultOpenApiMiddleware,
482
+ DefaultSwaggerMiddleware.Serve,
483
+ DefaultSwaggerMiddleware.Setup,
484
+ ];
485
+ middlewares.map(Sapling.resolve).forEach((r) => app.use(r));
486
+
487
+ // Register your controllers after the OpenAPI middlewares
488
+ const controllers = [BaseController, UserController];
489
+ controllers.map(Sapling.resolve).forEach((r) => app.use(r));
490
+ ```
491
+
492
+ The OpenAPI spec is automatically generated from your `@RequestBody`, `@RequestParam`, `@RequestQuery`, and `@ResponseBody` decorators.
493
+
494
+ #### Adding Documentation with Decorators
495
+
496
+ Use `@ControllerSchema` and `@RouteSchema` to add rich documentation:
497
+
498
+ ```typescript
499
+ import {
500
+ Controller,
501
+ ControllerSchema,
502
+ RouteSchema,
503
+ GET,
504
+ POST,
505
+ RequestBody,
506
+ ResponseBody,
507
+ HttpStatus,
508
+ } from "@tahminator/sapling";
509
+ import { z } from "zod";
510
+
511
+ const UserSchema = z.object({
512
+ id: z.string().describe("User ID"),
513
+ name: z.string().describe("User's full name"),
514
+ email: z.string().email().describe("User's email address"),
515
+ });
516
+
517
+ const CreateUserSchema = z.object({
518
+ name: z.string().min(1).describe("User's full name"),
519
+ email: z.string().email().describe("User's email address"),
520
+ });
521
+
522
+ const ErrorSchema = z.object({
523
+ message: z.string().describe("Error message"),
524
+ });
525
+
526
+ @ControllerSchema({
527
+ title: "Users",
528
+ description: "User management endpoints",
529
+ })
530
+ @Controller({ prefix: "/users" })
531
+ class UserController {
532
+ @RouteSchema({
533
+ summary: "List all users",
534
+ description: "Returns a paginated list of all users in the system",
535
+ responses: [
536
+ {
537
+ statusCode: HttpStatus.OK,
538
+ description: "List of users",
539
+ schema: z.array(UserSchema),
540
+ },
541
+ ],
542
+ })
543
+ @ResponseBody(z.array(UserSchema))
544
+ @GET()
545
+ listUsers(): ResponseEntity<z.infer<typeof UserSchema>[]> {
546
+ return ResponseEntity.ok().body([...]);
547
+ }
548
+
549
+ @RouteSchema({
550
+ summary: "Create a new user",
551
+ description: "Creates a new user with the provided information",
552
+ responses: [
553
+ {
554
+ statusCode: HttpStatus.CREATED,
555
+ description: "User created successfully",
556
+ schema: UserSchema,
557
+ },
558
+ {
559
+ statusCode: HttpStatus.BAD_REQUEST,
560
+ description: "Invalid request data",
561
+ schema: ErrorSchema,
562
+ },
563
+ ],
564
+ })
565
+ @RequestBody(CreateUserSchema)
566
+ @ResponseBody(UserSchema)
567
+ @POST()
568
+ createUser(request: Request): ResponseEntity<z.infer<typeof UserSchema>> {
569
+ const body = request.body as z.infer<typeof CreateUserSchema>;
570
+ return ResponseEntity.status(HttpStatus.CREATED).body({...});
571
+ }
572
+ }
573
+ ```
574
+
575
+ #### Customizing Paths
576
+
577
+ By default, OpenAPI spec is served at `/openapi.json` and Swagger UI at `/swagger.html`. Customize these paths:
578
+
579
+ ```typescript
580
+ Sapling.Extras.swaggerAndOpenApi.setOpenApiPath("/api-spec.json");
581
+ Sapling.Extras.swaggerAndOpenApi.setSwaggerPath("/api-docs");
582
+ ```
583
+
435
584
  ### Custom Serialization
436
585
 
437
586
  By default, Sapling uses `JSON.stringify` and `JSON.parse` for serialization. You can override these with custom serializers like [superjson](https://github.com/flightcontrolhq/superjson#readme) to automatically handle Dates, BigInts, and more: