express-zod-api 21.0.0-beta.3 → 21.0.0-beta.5

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/CHANGELOG.md CHANGED
@@ -5,28 +5,25 @@
5
5
  ### v21.0.0
6
6
 
7
7
  - Minimum supported versions of `express`: 4.21.1 and 5.0.1 (fixed vulnerabilities);
8
- - Running HTTP server made optional (can now configure HTTPS only):
9
- - Object argument of the `createConfig()` method changed:
10
- - The `server` property renamed to `http` and made optional;
11
- - Its nested properties moved to the top level of the config object:
12
- `jsonParser`, `upload`, `compression`, `rawParser` and `beforeRouting`.
13
- - The object resolved from the `createServer()` method changed:
14
- - Properties `httpServer` and `httpsServer` are removed;
15
- - Added `servers` property instead array containing those server instances in the same order.
16
- - The `serializer` property of `Documentation` and `Integration` constructor argument removed;
17
- - The `originalError` property of `InputValidationError` and `OutputValidationError` removed (use `cause` instead);
18
- - The `getStatusCodeFromError()` method removed (use the `ensureHttpError().statusCode` instead);
19
- - Both `logger` and `getChildLogger` properties of `beforeRouting` argument are replaced with all-purpose `getLogger`:
20
- - It returns the child logger for the given request (if configured) or the configured logger otherwise.
21
- - Specifying `method` or `methods` for `EndpointsFactory::build()` made optional and when it's omitted:
22
- - If the endpoint is assigned to a route using `DependsOnMethod` instance, the corresponding method is used;
23
- - Otherwise `GET` method is implied by default.
24
- - Other potentially breaking changes:
25
- - The optional property `methods` of `EndpointsFactory::build()` must be non-empty array when used;
26
- - The `Endpoint::getMethods()` method may now return `undefined`;
8
+ - Breaking changes to `createConfig()` argument:
9
+ - The `server` property renamed to `http` and made optional — (can now configure HTTPS only);
10
+ - These properties moved to the top level: `jsonParser`, `upload`, `compression`, `rawParser` and `beforeRouting`;
11
+ - Both `logger` and `getChildLogger` arguments of `beforeRouting` function are replaced with all-purpose `getLogger`.
12
+ - Breaking changes to `createServer()` resolved return:
13
+ - Both `httpServer` and `httpsServer` are combined into single `servers` property (array, same order).
14
+ - Breaking changes to `EndpointsFactory::build()` argument:
15
+ - Plural `methods`, `tags` and `scopes` properties replaced with singular `method`, `tag`, `scope` accordingly;
16
+ - The `method` property also made optional and can now be derived from `DependsOnMethod` or imply `GET` by default;
17
+ - When `method` is assigned with an array, it must be non-empty.
18
+ - Breaking changes to `positive` and `negative` propeties of `ResultHandler` constructor argument:
19
+ - Plural `statusCodes` and `mimeTypes` props within the values are replaced with singular `statusCode` and `mimeType`.
20
+ - Other breaking changes:
21
+ - The `serializer` property of `Documentation` and `Integration` constructor argument removed;
22
+ - The `originalError` property of `InputValidationError` and `OutputValidationError` removed (use `cause` instead);
23
+ - The `getStatusCodeFromError()` method removed (use the `ensureHttpError().statusCode` instead);
27
24
  - The `testEndpoint()` method can no longer test CORS headers — that function moved to `Routing` traverse;
25
+ - For `Endpoint`: `getMethods()` may return `undefined`, `getMimeTypes()` removed, `getSchema()` variants reduced;
28
26
  - Public properties `pairs`, `firstEndpoint` and `siblingMethods` of `DependsOnMethod` replaced with `entries`.
29
- - Routing traverse improvement: performance +5%, memory consumption -17%.
30
27
  - Consider the automated migration using the built-in ESLint rule.
31
28
 
32
29
  ```js
@@ -40,8 +37,71 @@ export default [
40
37
  ];
41
38
  ```
42
39
 
40
+ ```ts
41
+ // The sample of new structure
42
+ const config = createConfig({
43
+ http: { listen: 80 }, // became optional
44
+ https: { listen: 443, options: {} },
45
+ upload: true,
46
+ compression: true,
47
+ beforeRouting: ({ app, getLogger }) => {
48
+ const logger = getLogger();
49
+ app.use((req, res, next) => {
50
+ const childLogger = getLogger(req);
51
+ });
52
+ },
53
+ });
54
+ const { servers } = await createServer(config, {});
55
+ ```
56
+
43
57
  ## Version 20
44
58
 
59
+ ### v20.22.0
60
+
61
+ - Featuring a helper to describe nested Routing for already assigned routes:
62
+ - Suppose you want to describe `Routing` for both `/v1/path` and `/v1/path/subpath` routes having Endpoints attached;
63
+ - Previously, an empty path segment was proposed for that purpose, but there is more elegant and readable way now;
64
+ - The `.nest()` method is available both on `Endpoint` and `DependsOnMethod` instances:
65
+
66
+ ```ts
67
+ import { Routing } from "express-zod-api";
68
+
69
+ // Describing routes /v1/path and /v1/path/subpath both having endpoints assigned:
70
+ const before: Routing = {
71
+ v1: {
72
+ path: {
73
+ "": endpointA,
74
+ subpath: endpointB,
75
+ },
76
+ },
77
+ };
78
+
79
+ const after: Routing = {
80
+ v1: {
81
+ path: endpointA.nest({
82
+ subpath: endpointB,
83
+ }),
84
+ },
85
+ };
86
+ ```
87
+
88
+ ### v20.21.2
89
+
90
+ - Fixed the example implementation in the generated client for endpoints using path params:
91
+ - The choice of parser was made based on the exported `const jsonEndpoints` indexed by `path`;
92
+ - The actual `path` used for the lookup already contained parameter substitutions so that JSON parser didn't work;
93
+ - The new example implementation suggests choosing the parser based on the actual `response.headers`;
94
+ - The issue was found and reported by [@HenriJ](https://github.com/HenriJ).
95
+
96
+ ```diff
97
+ - const parser = `${method} ${path}` in jsonEndpoints ? "json" : "text";
98
+ + const isJSON = response.headers
99
+ + .get("content-type")
100
+ + ?.startsWith("application/json");
101
+ - return response[parser]();
102
+ + return response[isJSON ? "json" : "text"]();
103
+ ```
104
+
45
105
  ### v20.21.1
46
106
 
47
107
  - Performance tuning: `Routing` traverse made about 12 times faster.
package/README.md CHANGED
@@ -32,17 +32,18 @@ Start your API server with I/O schema validation and custom middlewares in minut
32
32
  13. [Enabling compression](#enabling-compression)
33
33
  5. [Advanced features](#advanced-features)
34
34
  1. [Customizing input sources](#customizing-input-sources)
35
- 2. [Route path params](#route-path-params)
36
- 3. [Multiple schemas for one route](#multiple-schemas-for-one-route)
37
- 4. [Response customization](#response-customization)
38
- 5. [Error handling](#error-handling)
39
- 6. [Production mode](#production-mode)
40
- 7. [Non-object response](#non-object-response) including file downloads
41
- 8. [File uploads](#file-uploads)
42
- 9. [Serving static files](#serving-static-files)
43
- 10. [Connect to your own express app](#connect-to-your-own-express-app)
44
- 11. [Testing endpoints](#testing-endpoints)
45
- 12. [Testing middlewares](#testing-middlewares)
35
+ 2. [Nested routes](#nested-routes)
36
+ 3. [Route path params](#route-path-params)
37
+ 4. [Multiple schemas for one route](#multiple-schemas-for-one-route)
38
+ 5. [Response customization](#response-customization)
39
+ 6. [Error handling](#error-handling)
40
+ 7. [Production mode](#production-mode)
41
+ 8. [Non-object response](#non-object-response) including file downloads
42
+ 9. [File uploads](#file-uploads)
43
+ 10. [Serving static files](#serving-static-files)
44
+ 11. [Connect to your own express app](#connect-to-your-own-express-app)
45
+ 12. [Testing endpoints](#testing-endpoints)
46
+ 13. [Testing middlewares](#testing-middlewares)
46
47
  6. [Special needs](#special-needs)
47
48
  1. [Different responses for different status codes](#different-responses-for-different-status-codes)
48
49
  2. [Array response](#array-response) for migrating legacy APIs
@@ -84,6 +85,7 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular
84
85
 
85
86
  These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:
86
87
 
88
+ [<img src="https://github.com/HenriJ.png" alt="@HenriJ" width="50px" />](https://github.com/HenriJ)
87
89
  [<img src="https://github.com/JonParton.png" alt="@JonParton" width="50px" />](https://github.com/JonParton)
88
90
  [<img src="https://github.com/williamgcampbell.png" alt="@williamgcampbell" width="50px" />](https://github.com/williamgcampbell)
89
91
  [<img src="https://github.com/t1nky.png" alt="@t1nky" width="50px" />](https://github.com/t1nky)
@@ -206,7 +208,7 @@ The endpoint responds with "Hello, World" or "Hello, {name}" if the name is supp
206
208
  import { z } from "zod";
207
209
 
208
210
  const helloWorldEndpoint = defaultEndpointsFactory.build({
209
- // method: "get" (default) or methods: ["get", "post", ...]
211
+ // method: "get" (default) or array ["get", "post", ...]
210
212
  input: z.object({
211
213
  name: z.string().optional(),
212
214
  }),
@@ -286,12 +288,9 @@ const authMiddleware = new Middleware({
286
288
  handler: async ({ input: { key }, request, logger }) => {
287
289
  logger.debug("Checking the key and token");
288
290
  const user = await db.Users.findOne({ key });
289
- if (!user) {
290
- throw createHttpError(401, "Invalid key");
291
- }
292
- if (request.headers.token !== user.token) {
291
+ if (!user) throw createHttpError(401, "Invalid key");
292
+ if (request.headers.token !== user.token)
293
293
  throw createHttpError(401, "Invalid token");
294
- }
295
294
  return { user }; // provides endpoints with options.user
296
295
  },
297
296
  });
@@ -303,10 +302,9 @@ By using `.addMiddleware()` method before `.build()` you can connect it to the e
303
302
  const yourEndpoint = defaultEndpointsFactory
304
303
  .addMiddleware(authMiddleware)
305
304
  .build({
306
- // ...,
307
- handler: async ({ options }) => {
308
- // options.user is the user returned by authMiddleware
309
- },
305
+ handler: async ({ options: { user } }) => {
306
+ // user is the one returned by authMiddleware
307
+ }, // ...
310
308
  });
311
309
  ```
312
310
 
@@ -320,7 +318,7 @@ import { defaultEndpointsFactory } from "express-zod-api";
320
318
  const factory = defaultEndpointsFactory
321
319
  .addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:
322
320
  .addMiddleware({
323
- handler: async ({ options: { user } }) => ({}), // options.user from authMiddleware
321
+ handler: async ({ options: { user } }) => ({}), // user from authMiddleware
324
322
  });
325
323
  ```
326
324
 
@@ -534,18 +532,14 @@ const updateUserEndpoint = defaultEndpointsFactory.build({
534
532
  method: "post",
535
533
  input: z.object({
536
534
  userId: z.string(),
537
- birthday: ez.dateIn(), // string -> Date
535
+ birthday: ez.dateIn(), // string -> Date in handler
538
536
  }),
539
537
  output: z.object({
540
- createdAt: ez.dateOut(), // Date -> string
538
+ createdAt: ez.dateOut(), // Date -> string in response
539
+ }),
540
+ handler: async ({ input }) => ({
541
+ createdAt: new Date("2022-01-22"), // 2022-01-22T00:00:00.000Z
541
542
  }),
542
- handler: async ({ input }) => {
543
- // input.birthday is Date
544
- return {
545
- // transmitted as "2022-01-22T00:00:00.000Z"
546
- createdAt: new Date("2022-01-22"),
547
- };
548
- },
549
543
  });
550
544
  ```
551
545
 
@@ -563,7 +557,6 @@ That function has several parameters and can be asynchronous.
563
557
  import { createConfig } from "express-zod-api";
564
558
 
565
559
  const config = createConfig({
566
- // ... other options
567
560
  cors: ({ defaultHeaders, request, endpoint, logger }) => ({
568
561
  ...defaultHeaders,
569
562
  "Access-Control-Max-Age": "5000",
@@ -589,8 +582,7 @@ const config = createConfig({
589
582
  key: fs.readFileSync("privkey.pem", "utf-8"),
590
583
  },
591
584
  listen: 443, // port, UNIX socket or options
592
- },
593
- // ... cors, logger, etc
585
+ }, // ... cors, logger, etc
594
586
  });
595
587
 
596
588
  // 'await' is only needed if you're going to use the returned entities.
@@ -717,14 +709,13 @@ In order to receive a compressed response the client should include the followin
717
709
 
718
710
  You can customize the list of `request` properties that are combined into `input` that is being validated and available
719
711
  to your endpoints and middlewares. The order here matters: each next item in the array has a higher priority than its
720
- previous sibling.
712
+ previous sibling. The following arrangement is default:
721
713
 
722
714
  ```typescript
723
715
  import { createConfig } from "express-zod-api";
724
716
 
725
717
  createConfig({
726
718
  inputSources: {
727
- // the defaults are:
728
719
  get: ["query", "params"],
729
720
  post: ["body", "params", "files"],
730
721
  put: ["body", "params"],
@@ -734,21 +725,32 @@ createConfig({
734
725
  });
735
726
  ```
736
727
 
728
+ ## Nested routes
729
+
730
+ Suppose you want to assign both `/v1/path` and `/v1/path/subpath` routes with Endpoints:
731
+
732
+ ```typescript
733
+ import { Routing } from "express-zod-api";
734
+
735
+ const routing: Routing = {
736
+ v1: {
737
+ path: endpointA.nest({
738
+ subpath: endpointB,
739
+ }),
740
+ },
741
+ };
742
+ ```
743
+
737
744
  ## Route path params
738
745
 
739
- You can describe the route of the endpoint using parameters:
746
+ You can assign your Endpoint to a route like `/v1/user/:id` where `:id` is the path parameter:
740
747
 
741
748
  ```typescript
742
749
  import { Routing } from "express-zod-api";
743
750
 
744
751
  const routing: Routing = {
745
752
  v1: {
746
- user: {
747
- // route path /v1/user/:id, where :id is the path param
748
- ":id": getUserEndpoint,
749
- // use the empty string to represent /v1/user if needed:
750
- // "": listAllUsersEndpoint,
751
- },
753
+ user: { ":id": getUserEndpoint },
752
754
  },
753
755
  };
754
756
  ```
@@ -763,12 +765,8 @@ const getUserEndpoint = endpointsFactory.build({
763
765
  // other inputs (in query):
764
766
  withExtendedInformation: z.boolean().optional(),
765
767
  }),
766
- output: z.object({
767
- /* ... */
768
- }),
769
- handler: async ({ input: { id } }) => {
770
- // id is the route path param, number
771
- },
768
+ output: z.object({}),
769
+ handler: async ({ input: { id } }) => ({}), // id is number,
772
770
  });
773
771
  ```
774
772
 
@@ -821,7 +819,7 @@ import {
821
819
  const yourResultHandler = new ResultHandler({
822
820
  positive: (data) => ({
823
821
  schema: z.object({ data }),
824
- mimeType: "application/json", // optinal, or mimeTypes for array
822
+ mimeType: "application/json", // optinal or array
825
823
  }),
826
824
  negative: z.object({ error: z.string() }),
827
825
  handler: ({ error, input, output, request, response, logger }) => {
@@ -904,17 +902,12 @@ const fileStreamingEndpointsFactory = new EndpointsFactory(
904
902
  positive: { schema: ez.file("buffer"), mimeType: "image/*" },
905
903
  negative: { schema: z.string(), mimeType: "text/plain" },
906
904
  handler: ({ response, error, output }) => {
907
- if (error) {
908
- response.status(400).send(error.message);
909
- return;
910
- }
911
- if ("filename" in output) {
905
+ if (error) return void response.status(400).send(error.message);
906
+ if ("filename" in output)
912
907
  fs.createReadStream(output.filename).pipe(
913
908
  response.type(output.filename),
914
909
  );
915
- } else {
916
- response.status(400).send("Filename is missing");
917
- }
910
+ else response.status(400).send("Filename is missing");
918
911
  },
919
912
  }),
920
913
  );
@@ -948,9 +941,7 @@ const config = createConfig({
948
941
  limits: { fileSize: 51200 }, // 50 KB
949
942
  limitError: createHttpError(413, "The file is too large"), // handled by errorHandler in config
950
943
  beforeUpload: ({ request, logger }) => {
951
- if (!canUpload(request)) {
952
- throw createHttpError(403, "Not authorized");
953
- }
944
+ if (!canUpload(request)) throw createHttpError(403, "Not authorized");
954
945
  },
955
946
  },
956
947
  });
@@ -1094,7 +1085,7 @@ import { ResultHandler } from "express-zod-api";
1094
1085
 
1095
1086
  new ResultHandler({
1096
1087
  positive: (data) => ({
1097
- statusCodes: [201, 202], // created or will be created
1088
+ statusCode: [201, 202], // created or will be created
1098
1089
  schema: z.object({ status: z.literal("created"), data }),
1099
1090
  }),
1100
1091
  negative: [
@@ -1103,7 +1094,7 @@ new ResultHandler({
1103
1094
  schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
1104
1095
  },
1105
1096
  {
1106
- statusCodes: [400, 500], // validation or internal error
1097
+ statusCode: [400, 500], // validation or internal error
1107
1098
  schema: z.object({ status: z.literal("error"), reason: z.string() }),
1108
1099
  },
1109
1100
  ],
@@ -1283,9 +1274,7 @@ const exampleEndpoint = defaultEndpointsFactory.build({
1283
1274
  .object({
1284
1275
  id: z.number().describe("the ID of the user"),
1285
1276
  })
1286
- .example({
1287
- id: 123,
1288
- }),
1277
+ .example({ id: 123 }),
1289
1278
  // ..., similarly for output and middlewares
1290
1279
  });
1291
1280
  ```
@@ -1308,9 +1297,8 @@ import {
1308
1297
  } from "express-zod-api";
1309
1298
 
1310
1299
  const config = createConfig({
1311
- // ..., use the simple or the advanced syntax:
1312
1300
  tags: {
1313
- users: "Everything about the users",
1301
+ users: "Everything about the users", // or advanced syntax:
1314
1302
  files: {
1315
1303
  description: "Everything about the files processing",
1316
1304
  url: "https://example.com",
@@ -1325,8 +1313,7 @@ const taggedEndpointsFactory = new EndpointsFactory({
1325
1313
  });
1326
1314
 
1327
1315
  const exampleEndpoint = taggedEndpointsFactory.build({
1328
- // ...
1329
- tag: "users", // or tags: ["users", "files"]
1316
+ tag: "users", // or array ["users", "files"]
1330
1317
  });
1331
1318
  ```
1332
1319
 
@@ -1364,12 +1351,10 @@ const ruleForClient: Producer = (
1364
1351
  ) => ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
1365
1352
 
1366
1353
  new Documentation({
1367
- /* config, routing, title, version */
1368
1354
  brandHandling: { [myBrand]: ruleForDocs },
1369
1355
  });
1370
1356
 
1371
1357
  new Integration({
1372
- /* routing */
1373
1358
  brandHandling: { [myBrand]: ruleForClient },
1374
1359
  });
1375
1360
  ```
@@ -1404,7 +1389,7 @@ const output = z.object({
1404
1389
  });
1405
1390
 
1406
1391
  endpointsFactory.build({
1407
- methods,
1392
+ method,
1408
1393
  input,
1409
1394
  output,
1410
1395
  handler: async (): Promise<z.input<typeof output>> => ({