express-zod-api 24.0.0-beta.1 → 24.0.0-beta.11
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 +48 -10
- package/README.md +46 -41
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +20 -57
- package/dist/index.d.ts +20 -57
- package/dist/index.js +8 -8
- package/migration/index.cjs +1 -1
- package/migration/index.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,27 +5,39 @@
|
|
|
5
5
|
### v24.0.0
|
|
6
6
|
|
|
7
7
|
- Switched to Zod 4:
|
|
8
|
-
- Minimum supported version of `zod` is 3.25.
|
|
9
|
-
- Explanation of the versioning strategy
|
|
8
|
+
- Minimum supported version of `zod` is 3.25.35, BUT imports MUST be from `zod/v4`;
|
|
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
|
-
-
|
|
19
|
-
-
|
|
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 `
|
|
23
|
-
|
|
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
|
-
-
|
|
28
|
-
- `z.any()` and `z.unknown()` are
|
|
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. [
|
|
40
|
-
6. [
|
|
41
|
-
7. [
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
});
|