express-zod-api 24.0.0-beta.1 → 24.0.0-beta.10

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
@@ -6,26 +6,38 @@
6
6
 
7
7
  - Switched to Zod 4:
8
8
  - Minimum supported version of `zod` is 3.25.1, BUT imports MUST be from `zod/v4`;
9
- - Explanation of the versioning strategy: https://github.com/colinhacks/zod/issues/4371;
9
+ - Read the [Explanation of the versioning strategy](https://github.com/colinhacks/zod/issues/4371);
10
10
  - Express Zod API, however, is not aiming to support both Zod 3 and Zod 4 simultaneously due to:
11
11
  - incompatibility of data structures;
12
12
  - operating composite schemas (need to avoid mixing schemas of different versions);
13
13
  - the temporary nature of this transition;
14
14
  - the advantages of Zod 4 that provide opportunities to simplifications and corrections of known issues.
15
15
  - `IOSchema` type had to be simplified down to a schema resulting to an `object`, but not an `array`;
16
- - Despite supporting examples by the new Zod method `.meta()`, users should still use `.example()` to set them;
17
16
  - Refer to [Migration guide on Zod 4](https://v4.zod.dev/v4/changelog) for adjusting your schemas;
18
- - Generating Documentation is partially delegated to Zod 4 `z.toJSONSchema()`:
19
- - The basic depiction of each schema is now natively performed by Zod 4;
17
+ - Changes to `ZodType::example()` (Zod plugin method):
18
+ - Now acts as an alias for `ZodType::meta({ examples })`;
19
+ - The argument has to be the output type of the schema (used to be the opposite):
20
+ - This change is only breaking for transforming schemas;
21
+ - In order to specify an example for an input schema the `.example()` method must be called before `.transform()`;
22
+ - The transforming proprietary schemas `ez.dateIn()` and `ez.dateOut()` now accept metadata as its argument:
23
+ - This allows to set examples before transformation (`ez.dateIn()`) and to avoid the examples "branding";
24
+ - Changes to `Documentation`:
25
+ - Generating Documentation is mostly delegated to Zod 4 `z.toJSONSchema()`;
20
26
  - Express Zod API implements some overrides and improvements to fit it into OpenAPI 3.1 that extends JSON Schema;
21
27
  - The `numericRange` option removed from `Documentation` class constructor argument;
22
- - The `brandHandling` should consist of postprocessing functions altering the depiction made by Zod 4;
23
- - The `Depicter` type signature changed;
24
- - The `optionalPropStyle` option removed from `Integration` class constructor:
28
+ - The `Depicter` type signature changed: became a postprocessing function returning an overridden JSON Schema;
29
+ - Changes to `Integration`:
30
+ - The `optionalPropStyle` option removed from `Integration` class constructor:
25
31
  - Use `.optional()` to add question mark to the object property as well as `undefined` to its type;
26
32
  - Use `.or(z.undefined())` to add `undefined` to the type of the object property;
27
- - Reasoning: https://x.com/colinhacks/status/1919292504861491252;
28
- - `z.any()` and `z.unknown()` are not optional, details: https://v4.zod.dev/v4/changelog#changes-zunknown-optionality.
33
+ - See the [reasoning](https://x.com/colinhacks/status/1919292504861491252);
34
+ - `z.any()` and `z.unknown()` are required: [details](https://v4.zod.dev/v4/changelog#changes-zunknown-optionality);
35
+ - Added types generation for `z.never()`, `z.void()` and `z.unknown()` schemas;
36
+ - The fallback type for unsupported schemas and unclear transformations in response changed from `any` to `unknown`;
37
+ - The argument of `ResultHandler::handler` is now discriminated: either `output` or `error` is `null`, not both;
38
+ - The `getExamples()` public helper removed — use `.meta()?.examples` instead;
39
+ - Added the new proprietary schema `ez.buffer()`;
40
+ - The `ez.file()` schema removed: use `z.string()`, `z.base64()`, `ez.buffer()` or their union;
29
41
  - Consider the automated migration using the built-in ESLint rule.
30
42
 
31
43
  ```js
@@ -44,8 +56,34 @@ export default [
44
56
  + import { z } from "zod/v4";
45
57
  ```
46
58
 
59
+ ```diff
60
+ input: z.string()
61
+ + .example("123")
62
+ .transform(Number)
63
+ - .example("123")
64
+ ```
65
+
66
+ ```diff
67
+ - ez.dateIn().example("2021-12-31");
68
+ + ez.dateIn({ examples: ["2021-12-31"] });
69
+ - ez.file("base64");
70
+ + z.base64();
71
+ - ez.file("buffer");
72
+ + ez.buffer();
73
+ ```
74
+
47
75
  ## Version 23
48
76
 
77
+ ### v23.6.1
78
+
79
+ - `createServer()` displays a warning when no server is configured.
80
+
81
+ ### v23.6.0
82
+
83
+ - Featuring `gracefulShutdown.beforeExit()` hook:
84
+ - The function to execute after the server was closed, but before terminating the process (can be asynchronous);
85
+ - The feature suggested by [@HeikoOsigus](https://github.com/HeikoOsigus).
86
+
49
87
  ### v23.5.0
50
88
 
51
89
  - Integer number `format` in generated Documentation now also depends on the `numericRange` option:
package/README.md CHANGED
@@ -36,9 +36,9 @@ Start your API server with I/O schema validation and custom middlewares in minut
36
36
  2. [Headers as input source](#headers-as-input-source)
37
37
  3. [Response customization](#response-customization)
38
38
  4. [Empty response](#empty-response)
39
- 5. [Error handling](#error-handling)
40
- 6. [Production mode](#production-mode)
41
- 7. [Non-object response](#non-object-response) including file downloads
39
+ 5. [Non-JSON response](#non-json-response) including file downloads
40
+ 6. [Error handling](#error-handling)
41
+ 7. [Production mode](#production-mode)
42
42
  8. [HTML Forms (URL encoded)](#html-forms-url-encoded)
43
43
  9. [File uploads](#file-uploads)
44
44
  10. [Connect to your own express app](#connect-to-your-own-express-app)
@@ -84,6 +84,7 @@ Therefore, many basic tasks can be accomplished faster and easier, in particular
84
84
 
85
85
  These people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:
86
86
 
87
+ [<img src="https://github.com/HeikoOsigus.png" alt="@HeikoOsigus" width="50px" />](https://github.com/HeikoOsigus)
87
88
  [<img src="https://github.com/crgeary.png" alt="@crgeary" width="50px" />](https://github.com/crgeary)
88
89
  [<img src="https://github.com/williamgcampbell.png" alt="@williamgcampbell" width="50px" />](https://github.com/williamgcampbell)
89
90
  [<img src="https://github.com/gmorgen1.png" alt="@gmorgen1" width="50px" />](https://github.com/gmorgen1)
@@ -478,7 +479,7 @@ By the way, you can also refine the whole I/O object, for example in case you ne
478
479
  const endpoint = endpointsFactory.build({
479
480
  input: z
480
481
  .object({
481
- email: z.string().email().optional(),
482
+ email: z.email().optional(),
482
483
  id: z.string().optional(),
483
484
  otherThing: z.string().optional(),
484
485
  })
@@ -561,7 +562,7 @@ in actual response by calling
561
562
  which in turn calls
562
563
  [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
563
564
  It is also impossible to transmit the `Date` in its original form to your endpoints within JSON. Therefore, there is
564
- confusion with original method ~~z.date()~~ that should not be used within IO schemas of your API.
565
+ confusion with original method ~~z.date()~~ that is not recommended to use without transformations.
565
566
 
566
567
  In order to solve this problem, the framework provides two custom methods for dealing with dates: `ez.dateIn()` and
567
568
  `ez.dateOut()` for using within input and output schemas accordingly.
@@ -577,7 +578,7 @@ provides your endpoint handler or middleware with a `Date`. It supports the foll
577
578
  ```
578
579
 
579
580
  `ez.dateOut()`, on the contrary, accepts a `Date` and provides `ResultHandler` with a `string` representation in ISO
580
- format for the response transmission. Consider the following simplified example for better understanding:
581
+ format for the response transmission. Both schemas accept metadata as an argument. Consider the following example:
581
582
 
582
583
  ```typescript
583
584
  import { z } from "zod/v4";
@@ -587,10 +588,10 @@ const updateUserEndpoint = defaultEndpointsFactory.build({
587
588
  method: "post",
588
589
  input: z.object({
589
590
  userId: z.string(),
590
- birthday: ez.dateIn(), // string -> Date in handler
591
+ birthday: ez.dateIn({ examples: ["1963-04-21"] }), // string -> Date in handler
591
592
  }),
592
593
  output: z.object({
593
- createdAt: ez.dateOut(), // Date -> string in response
594
+ createdAt: ez.dateOut({ examples: ["2021-12-31"] }), // Date -> string in response
594
595
  }),
595
596
  handler: async ({ input }) => ({
596
597
  createdAt: new Date("2022-01-22"), // 2022-01-22T00:00:00.000Z
@@ -871,6 +872,33 @@ const resultHandler = new ResultHandler({
871
872
  });
872
873
  ```
873
874
 
875
+ ## Non-JSON response
876
+
877
+ To configure a non-JSON responses (for example, to send an image file) you should specify its MIME type.
878
+
879
+ You can find two approaches to `EndpointsFactory` and `ResultHandler` implementation
880
+ [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts).
881
+ One of them implements file streaming, in this case the endpoint just has to provide the filename.
882
+ The response schema can be `z.string()`, `z.base64()` or `ez.buffer()` to reflect the data accordingly in the
883
+ [generated documentation](#creating-a-documentation).
884
+
885
+ ```typescript
886
+ const fileStreamingEndpointsFactory = new EndpointsFactory(
887
+ new ResultHandler({
888
+ positive: { schema: ez.buffer(), mimeType: "image/*" },
889
+ negative: { schema: z.string(), mimeType: "text/plain" },
890
+ handler: ({ response, error, output }) => {
891
+ if (error) return void response.status(400).send(error.message);
892
+ if ("filename" in output)
893
+ fs.createReadStream(output.filename).pipe(
894
+ response.attachment(output.filename),
895
+ );
896
+ else response.status(400).send("Filename is missing");
897
+ },
898
+ }),
899
+ );
900
+ ```
901
+
874
902
  ## Error handling
875
903
 
876
904
  All runtime errors are handled by a `ResultHandler`. The default is `defaultResultHandler`. Using `ensureHttpError()`
@@ -914,34 +942,6 @@ createHttpError(500, "Something is broken"); // —> "Internal Server Error"
914
942
  createHttpError(501, "We didn't make it yet", { expose: true }); // —> "We didn't make it yet"
915
943
  ```
916
944
 
917
- ## Non-object response
918
-
919
- Thus, you can configure non-object responses too, for example, to send an image file.
920
-
921
- You can find two approaches to `EndpointsFactory` and `ResultHandler` implementation
922
- [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts).
923
- One of them implements file streaming, in this case the endpoint just has to provide the filename.
924
- The response schema generally may be just `z.string()`, but I made more specific `ez.file()` that also supports
925
- `ez.file("binary")` and `ez.file("base64")` variants which are reflected in the
926
- [generated documentation](#creating-a-documentation).
927
-
928
- ```typescript
929
- const fileStreamingEndpointsFactory = new EndpointsFactory(
930
- new ResultHandler({
931
- positive: { schema: ez.file("buffer"), mimeType: "image/*" },
932
- negative: { schema: z.string(), mimeType: "text/plain" },
933
- handler: ({ response, error, output }) => {
934
- if (error) return void response.status(400).send(error.message);
935
- if ("filename" in output)
936
- fs.createReadStream(output.filename).pipe(
937
- response.type(output.filename),
938
- );
939
- else response.status(400).send("Filename is missing");
940
- },
941
- }),
942
- );
943
- ```
944
-
945
945
  ## HTML Forms (URL encoded)
946
946
 
947
947
  Use the proprietary schema `ez.form()` with an object shape or a custom `z.object()` with form fields in order to
@@ -957,7 +957,7 @@ export const submitFeedbackEndpoint = defaultEndpointsFactory.build({
957
957
  method: "post",
958
958
  input: ez.form({
959
959
  name: z.string().min(1),
960
- email: z.string().email(),
960
+ email: z.email(),
961
961
  message: z.string().min(1),
962
962
  }),
963
963
  });
@@ -1109,7 +1109,7 @@ new ResultHandler({
1109
1109
  negative: [
1110
1110
  {
1111
1111
  statusCode: 409, // conflict: entity already exists
1112
- schema: z.object({ status: z.literal("exists"), id: z.number().int() }),
1112
+ schema: z.object({ status: z.literal("exists"), id: z.int() }),
1113
1113
  },
1114
1114
  {
1115
1115
  statusCode: [400, 500], // validation or internal error
@@ -1147,7 +1147,7 @@ const rawAcceptingEndpoint = defaultEndpointsFactory.build({
1147
1147
  input: ez.raw({
1148
1148
  /* the place for additional inputs, like route params, if needed */
1149
1149
  }),
1150
- output: z.object({ length: z.number().int().nonnegative() }),
1150
+ output: z.object({ length: z.int().nonnegative() }),
1151
1151
  handler: async ({ input: { raw } }) => ({
1152
1152
  length: raw.length, // raw is Buffer
1153
1153
  }),
@@ -1167,6 +1167,7 @@ createConfig({
1167
1167
  gracefulShutdown: {
1168
1168
  timeout: 1000,
1169
1169
  events: ["SIGINT", "SIGTERM"],
1170
+ beforeExit: /* async */ () => {},
1170
1171
  },
1171
1172
  });
1172
1173
  ```
@@ -1185,7 +1186,7 @@ import { EventStreamFactory } from "express-zod-api";
1185
1186
  import { setTimeout } from "node:timers/promises";
1186
1187
 
1187
1188
  const subscriptionEndpoint = new EventStreamFactory({
1188
- time: z.number().int().positive(),
1189
+ time: z.int().positive(),
1189
1190
  }).buildVoid({
1190
1191
  input: z.object({}), // optional input schema
1191
1192
  handler: async ({ options: { emit, isClosed } }) => {
@@ -1271,7 +1272,11 @@ const exampleEndpoint = defaultEndpointsFactory.build({
1271
1272
  shortDescription: "Retrieves the user.", // <—— this becomes the summary line
1272
1273
  description: "The detailed explanaition on what this endpoint does.",
1273
1274
  input: z.object({
1274
- id: z.number().describe("the ID of the user").example(123),
1275
+ id: z
1276
+ .string()
1277
+ .example("123") // input examples should be set before transformations
1278
+ .transform(Number)
1279
+ .describe("the ID of the user"),
1275
1280
  }),
1276
1281
  // ..., similarly for output and middlewares
1277
1282
  });