express-zod-api 24.0.0-beta.8 → 24.0.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/CHANGELOG.md +14 -9
- package/README.md +185 -198
- package/dist/index.cjs +6 -6
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.js +6 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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);
|
|
@@ -21,19 +21,24 @@
|
|
|
21
21
|
- In order to specify an example for an input schema the `.example()` method must be called before `.transform()`;
|
|
22
22
|
- The transforming proprietary schemas `ez.dateIn()` and `ez.dateOut()` now accept metadata as its argument:
|
|
23
23
|
- This allows to set examples before transformation (`ez.dateIn()`) and to avoid the examples "branding";
|
|
24
|
-
-
|
|
25
|
-
-
|
|
24
|
+
- Changes to `Documentation`:
|
|
25
|
+
- Generating Documentation is mostly delegated to Zod 4 `z.toJSONSchema()`;
|
|
26
26
|
- Express Zod API implements some overrides and improvements to fit it into OpenAPI 3.1 that extends JSON Schema;
|
|
27
27
|
- The `numericRange` option removed from `Documentation` class constructor argument;
|
|
28
28
|
- The `Depicter` type signature changed: became a postprocessing function returning an overridden JSON Schema;
|
|
29
|
-
-
|
|
29
|
+
- Changes to `Integration`:
|
|
30
|
+
- The `optionalPropStyle` option removed from `Integration` class constructor:
|
|
30
31
|
- Use `.optional()` to add question mark to the object property as well as `undefined` to its type;
|
|
31
32
|
- Use `.or(z.undefined())` to add `undefined` to the type of the object property;
|
|
32
|
-
-
|
|
33
|
-
- `z.any()`
|
|
34
|
-
-
|
|
33
|
+
- See the [reasoning](https://x.com/colinhacks/status/1919292504861491252);
|
|
34
|
+
- Properties assigned with `z.any()` or `z.unknown()` schema are now typed as required:
|
|
35
|
+
- Read the [details here](https://v4.zod.dev/v4/changelog#changes-zunknown-optionality);
|
|
36
|
+
- Added types generation for `z.never()`, `z.void()` and `z.unknown()` schemas;
|
|
37
|
+
- The fallback type for unsupported schemas and unclear transformations in response changed from `any` to `unknown`;
|
|
38
|
+
- The argument of `ResultHandler::handler` is now discriminated: either `output` or `error` is `null`, not both;
|
|
35
39
|
- The `getExamples()` public helper removed — use `.meta()?.examples` instead;
|
|
36
|
-
-
|
|
40
|
+
- Added the new proprietary schema `ez.buffer()`;
|
|
41
|
+
- The `ez.file()` schema removed: use `z.string()`, `z.base64()`, `ez.buffer()` or their union;
|
|
37
42
|
- Consider the automated migration using the built-in ESLint rule.
|
|
38
43
|
|
|
39
44
|
```js
|
package/README.md
CHANGED
|
@@ -27,10 +27,9 @@ Start your API server with I/O schema validation and custom middlewares in minut
|
|
|
27
27
|
8. [Dealing with dates](#dealing-with-dates)
|
|
28
28
|
9. [Cross-Origin Resource Sharing](#cross-origin-resource-sharing) (CORS)
|
|
29
29
|
10. [Enabling HTTPS](#enabling-https)
|
|
30
|
-
11. [
|
|
31
|
-
12. [
|
|
32
|
-
13. [
|
|
33
|
-
14. [Enabling compression](#enabling-compression)
|
|
30
|
+
11. [Enabling compression](#enabling-compression)
|
|
31
|
+
12. [Customizing logger](#customizing-logger)
|
|
32
|
+
13. [Child logger](#child-logger)
|
|
34
33
|
5. [Advanced features](#advanced-features)
|
|
35
34
|
1. [Customizing input sources](#customizing-input-sources)
|
|
36
35
|
2. [Headers as input source](#headers-as-input-source)
|
|
@@ -44,19 +43,20 @@ Start your API server with I/O schema validation and custom middlewares in minut
|
|
|
44
43
|
10. [Connect to your own express app](#connect-to-your-own-express-app)
|
|
45
44
|
11. [Testing endpoints](#testing-endpoints)
|
|
46
45
|
12. [Testing middlewares](#testing-middlewares)
|
|
47
|
-
6. [
|
|
48
|
-
1. [Different responses for different status codes](#different-responses-for-different-status-codes)
|
|
49
|
-
2. [Array response](#array-response) for migrating legacy APIs
|
|
50
|
-
3. [Accepting raw data](#accepting-raw-data)
|
|
51
|
-
4. [Graceful shutdown](#graceful-shutdown)
|
|
52
|
-
5. [Subscriptions](#subscriptions)
|
|
53
|
-
7. [Integration and Documentation](#integration-and-documentation)
|
|
46
|
+
6. [Integration and Documentation](#integration-and-documentation)
|
|
54
47
|
1. [Zod Plugin](#zod-plugin)
|
|
55
48
|
2. [Generating a Frontend Client](#generating-a-frontend-client)
|
|
56
49
|
3. [Creating a documentation](#creating-a-documentation)
|
|
57
50
|
4. [Tagging the endpoints](#tagging-the-endpoints)
|
|
58
51
|
5. [Deprecated schemas and routes](#deprecated-schemas-and-routes)
|
|
59
52
|
6. [Customizable brands handling](#customizable-brands-handling)
|
|
53
|
+
7. [Special needs](#special-needs)
|
|
54
|
+
1. [Different responses for different status codes](#different-responses-for-different-status-codes)
|
|
55
|
+
2. [Array response](#array-response) for migrating legacy APIs
|
|
56
|
+
3. [Accepting raw data](#accepting-raw-data)
|
|
57
|
+
4. [Profiling](#profiling)
|
|
58
|
+
5. [Graceful shutdown](#graceful-shutdown)
|
|
59
|
+
6. [Subscriptions](#subscriptions)
|
|
60
60
|
8. [Caveats](#caveats)
|
|
61
61
|
1. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
|
|
62
62
|
9. [Your input to my output](#your-input-to-my-output)
|
|
@@ -186,34 +186,25 @@ Ensure having the following options in your `tsconfig.json` file in order to mak
|
|
|
186
186
|
|
|
187
187
|
## Set up config
|
|
188
188
|
|
|
189
|
-
Create a minimal configuration.
|
|
190
|
-
[in sources](https://github.com/RobinTail/express-zod-api/blob/master/express-zod-api/src/config-type.ts).
|
|
189
|
+
Create a minimal configuration. Find out all configurable options
|
|
190
|
+
[in sources](https://github.com/RobinTail/express-zod-api/blob/master/express-zod-api/src/config-type.ts).
|
|
191
191
|
|
|
192
192
|
```typescript
|
|
193
193
|
import { createConfig } from "express-zod-api";
|
|
194
194
|
|
|
195
195
|
const config = createConfig({
|
|
196
|
-
http: {
|
|
197
|
-
|
|
198
|
-
},
|
|
199
|
-
cors: true,
|
|
196
|
+
http: { listen: 8090 }, // port, UNIX socket or Net::ListenOptions
|
|
197
|
+
cors: false, // decide whether to enable CORS
|
|
200
198
|
});
|
|
201
199
|
```
|
|
202
200
|
|
|
203
|
-
## Create an endpoints factory
|
|
204
|
-
|
|
205
|
-
In the basic case, you can just import and use the default factory.
|
|
206
|
-
_See also [Middlewares](#middlewares) and [Response customization](#response-customization)._
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { defaultEndpointsFactory } from "express-zod-api";
|
|
210
|
-
```
|
|
211
|
-
|
|
212
201
|
## Create your first endpoint
|
|
213
202
|
|
|
214
|
-
|
|
203
|
+
Use the default factory to make an endpoint that responds with "Hello, World" or "Hello, {name}" depending on inputs.
|
|
204
|
+
Learn how to make factories for [custom response](#response-customization) and by [adding middlewares](#middlewares).
|
|
215
205
|
|
|
216
206
|
```typescript
|
|
207
|
+
import { defaultEndpointsFactory } from "express-zod-api";
|
|
217
208
|
import { z } from "zod/v4";
|
|
218
209
|
|
|
219
210
|
const helloWorldEndpoint = defaultEndpointsFactory.build({
|
|
@@ -601,18 +592,15 @@ const updateUserEndpoint = defaultEndpointsFactory.build({
|
|
|
601
592
|
|
|
602
593
|
## Cross-Origin Resource Sharing
|
|
603
594
|
|
|
604
|
-
You can enable your API for other domains using the corresponding configuration option `cors`.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
See [MDN article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for more information.
|
|
608
|
-
|
|
609
|
-
In addition to being a boolean, `cors` can also be assigned a function that overrides default CORS headers.
|
|
610
|
-
That function has several parameters and can be asynchronous.
|
|
595
|
+
You can enable your API for other domains using the corresponding configuration option `cors`. The value is required to
|
|
596
|
+
ensure you explicitly choose the correct setting. In addition to being a boolean, `cors` can also be assigned a
|
|
597
|
+
function that overrides default CORS headers. That function has several parameters and can be asynchronous.
|
|
611
598
|
|
|
612
599
|
```typescript
|
|
613
600
|
import { createConfig } from "express-zod-api";
|
|
614
601
|
|
|
615
602
|
const config = createConfig({
|
|
603
|
+
/** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS */
|
|
616
604
|
cors: ({ defaultHeaders, request, endpoint, logger }) => ({
|
|
617
605
|
...defaultHeaders,
|
|
618
606
|
"Access-Control-Max-Age": "5000",
|
|
@@ -650,6 +638,25 @@ Ensure having `@types/node` package installed. At least you need to specify the
|
|
|
650
638
|
certificate and the key, issued by the certifying authority. For example, you can acquire a free TLS certificate for
|
|
651
639
|
your API at [Let's Encrypt](https://letsencrypt.org/).
|
|
652
640
|
|
|
641
|
+
## Enabling compression
|
|
642
|
+
|
|
643
|
+
According to [Express.js best practices guide](https://expressjs.com/en/advanced/best-practice-performance.html)
|
|
644
|
+
it might be a good idea to enable GZIP and Brotli compression for your API responses.
|
|
645
|
+
|
|
646
|
+
Install `compression` and `@types/compression`, and enable or configure compression:
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { createConfig } from "express-zod-api";
|
|
650
|
+
|
|
651
|
+
const config = createConfig({
|
|
652
|
+
/** @link https://www.npmjs.com/package/compression#options */
|
|
653
|
+
compression: { threshold: "1kb" }, // or true
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
In order to receive a compressed response the client should include the following header in the request:
|
|
658
|
+
`Accept-Encoding: br, gzip, deflate`. Only responses with compressible content types are subject to compression.
|
|
659
|
+
|
|
653
660
|
## Customizing logger
|
|
654
661
|
|
|
655
662
|
A simple built-in console logger is used by default with the following options that you can configure:
|
|
@@ -708,57 +715,6 @@ const config = createConfig({
|
|
|
708
715
|
});
|
|
709
716
|
```
|
|
710
717
|
|
|
711
|
-
## Profiling
|
|
712
|
-
|
|
713
|
-
For debugging and performance testing purposes the framework offers a simple `.profile()` method on the built-in logger.
|
|
714
|
-
It starts a timer when you call it and measures the duration in adaptive units (from picoseconds to minutes) until you
|
|
715
|
-
invoke the returned callback. The default severity of those measurements is `debug`.
|
|
716
|
-
|
|
717
|
-
```typescript
|
|
718
|
-
import { createConfig, BuiltinLogger } from "express-zod-api";
|
|
719
|
-
|
|
720
|
-
// This enables the .profile() method on built-in logger:
|
|
721
|
-
declare module "express-zod-api" {
|
|
722
|
-
interface LoggerOverrides extends BuiltinLogger {}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Inside a handler of Endpoint, Middleware or ResultHandler:
|
|
726
|
-
const done = logger.profile("expensive operation");
|
|
727
|
-
doExpensiveOperation();
|
|
728
|
-
done(); // debug: expensive operation '555 milliseconds'
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
You can also customize the profiler with your own formatter, chosen severity or even performance assessment function:
|
|
732
|
-
|
|
733
|
-
```typescript
|
|
734
|
-
logger.profile({
|
|
735
|
-
message: "expensive operation",
|
|
736
|
-
severity: (ms) => (ms > 500 ? "error" : "info"), // assess immediately
|
|
737
|
-
formatter: (ms) => `${ms.toFixed(2)}ms`, // custom format
|
|
738
|
-
});
|
|
739
|
-
doExpensiveOperation();
|
|
740
|
-
done(); // error: expensive operation '555.55ms'
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
## Enabling compression
|
|
744
|
-
|
|
745
|
-
According to [Express.js best practices guide](https://expressjs.com/en/advanced/best-practice-performance.html)
|
|
746
|
-
it might be a good idea to enable GZIP and Brotli compression for your API responses.
|
|
747
|
-
|
|
748
|
-
Install `compression` and `@types/compression`, and enable or configure compression:
|
|
749
|
-
|
|
750
|
-
```typescript
|
|
751
|
-
import { createConfig } from "express-zod-api";
|
|
752
|
-
|
|
753
|
-
const config = createConfig({
|
|
754
|
-
/** @link https://www.npmjs.com/package/compression#options */
|
|
755
|
-
compression: { threshold: "1kb" }, // or true
|
|
756
|
-
});
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
In order to receive a compressed response the client should include the following header in the request:
|
|
760
|
-
`Accept-Encoding: br, gzip, deflate`. Only responses with compressible content types are subject to compression.
|
|
761
|
-
|
|
762
718
|
# Advanced features
|
|
763
719
|
|
|
764
720
|
## Customizing input sources
|
|
@@ -1089,118 +1045,6 @@ expect(loggerMock._getLogs().error).toHaveLength(0);
|
|
|
1089
1045
|
expect(output).toEqual({ collectedOptions: ["prev"], testLength: 9 });
|
|
1090
1046
|
```
|
|
1091
1047
|
|
|
1092
|
-
# Special needs
|
|
1093
|
-
|
|
1094
|
-
## Different responses for different status codes
|
|
1095
|
-
|
|
1096
|
-
In some special cases you may want the ResultHandler to respond slightly differently depending on the status code,
|
|
1097
|
-
for example if your API strictly follows REST standards. It may also be necessary to reflect this difference in the
|
|
1098
|
-
generated Documentation. For that purpose, the constructor of `ResultHandler` accepts flexible declaration of possible
|
|
1099
|
-
response schemas and their corresponding status codes.
|
|
1100
|
-
|
|
1101
|
-
```typescript
|
|
1102
|
-
import { ResultHandler } from "express-zod-api";
|
|
1103
|
-
|
|
1104
|
-
new ResultHandler({
|
|
1105
|
-
positive: (data) => ({
|
|
1106
|
-
statusCode: [201, 202], // created or will be created
|
|
1107
|
-
schema: z.object({ status: z.literal("created"), data }),
|
|
1108
|
-
}),
|
|
1109
|
-
negative: [
|
|
1110
|
-
{
|
|
1111
|
-
statusCode: 409, // conflict: entity already exists
|
|
1112
|
-
schema: z.object({ status: z.literal("exists"), id: z.int() }),
|
|
1113
|
-
},
|
|
1114
|
-
{
|
|
1115
|
-
statusCode: [400, 500], // validation or internal error
|
|
1116
|
-
schema: z.object({ status: z.literal("error"), reason: z.string() }),
|
|
1117
|
-
},
|
|
1118
|
-
],
|
|
1119
|
-
handler: ({ error, response, output }) => {
|
|
1120
|
-
// your implementation here
|
|
1121
|
-
},
|
|
1122
|
-
});
|
|
1123
|
-
```
|
|
1124
|
-
|
|
1125
|
-
## Array response
|
|
1126
|
-
|
|
1127
|
-
Please avoid doing this in new projects: responding with array is a bad practice keeping your endpoints from evolving
|
|
1128
|
-
in backward compatible way (without making breaking changes). Nevertheless, for the purpose of easier migration of
|
|
1129
|
-
legacy APIs to this framework consider using `arrayResultHandler` or `arrayEndpointsFactory` instead of default ones,
|
|
1130
|
-
or implement your own ones in a similar way.
|
|
1131
|
-
The `arrayResultHandler` expects your endpoint to have `items` property in the `output` object schema. The array
|
|
1132
|
-
assigned to that property is used as the response. This approach also supports examples, as well as documentation and
|
|
1133
|
-
client generation. Check out [the example endpoint](/example/endpoints/list-users.ts) for more details.
|
|
1134
|
-
|
|
1135
|
-
## Accepting raw data
|
|
1136
|
-
|
|
1137
|
-
Some APIs may require an endpoint to be able to accept and process raw data, such as streaming or uploading a binary
|
|
1138
|
-
file as an entire body of request. Use the proprietary `ez.raw()` schema as the input schema of your endpoint.
|
|
1139
|
-
The default parser in this case is `express.raw()`. You can customize it by assigning the `rawParser` option in config.
|
|
1140
|
-
The raw data is placed into `request.body.raw` property, having type `Buffer`.
|
|
1141
|
-
|
|
1142
|
-
```typescript
|
|
1143
|
-
import { defaultEndpointsFactory, ez } from "express-zod-api";
|
|
1144
|
-
|
|
1145
|
-
const rawAcceptingEndpoint = defaultEndpointsFactory.build({
|
|
1146
|
-
method: "post",
|
|
1147
|
-
input: ez.raw({
|
|
1148
|
-
/* the place for additional inputs, like route params, if needed */
|
|
1149
|
-
}),
|
|
1150
|
-
output: z.object({ length: z.int().nonnegative() }),
|
|
1151
|
-
handler: async ({ input: { raw } }) => ({
|
|
1152
|
-
length: raw.length, // raw is Buffer
|
|
1153
|
-
}),
|
|
1154
|
-
});
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
## Graceful shutdown
|
|
1158
|
-
|
|
1159
|
-
You can enable and configure a special request monitoring that, if it receives a signal to terminate a process, will
|
|
1160
|
-
first put the server into a mode that rejects new requests, attempt to complete started requests within the specified
|
|
1161
|
-
time, and then forcefully stop the server and terminate the process.
|
|
1162
|
-
|
|
1163
|
-
```ts
|
|
1164
|
-
import { createConfig } from "express-zod-api";
|
|
1165
|
-
|
|
1166
|
-
createConfig({
|
|
1167
|
-
gracefulShutdown: {
|
|
1168
|
-
timeout: 1000,
|
|
1169
|
-
events: ["SIGINT", "SIGTERM"],
|
|
1170
|
-
beforeExit: /* async */ () => {},
|
|
1171
|
-
},
|
|
1172
|
-
});
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
## Subscriptions
|
|
1176
|
-
|
|
1177
|
-
If you want the user of a client application to be able to subscribe to subsequent updates initiated by the server,
|
|
1178
|
-
consider [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) feature.
|
|
1179
|
-
Client application can subscribe to the event stream using `EventSource` class instance or the
|
|
1180
|
-
[instance of the generated](#generating-a-frontend-client) `Subscription` class. The following example demonstrates
|
|
1181
|
-
the implementation emitting the `time` event each second.
|
|
1182
|
-
|
|
1183
|
-
```typescript
|
|
1184
|
-
import { z } from "zod/v4";
|
|
1185
|
-
import { EventStreamFactory } from "express-zod-api";
|
|
1186
|
-
import { setTimeout } from "node:timers/promises";
|
|
1187
|
-
|
|
1188
|
-
const subscriptionEndpoint = new EventStreamFactory({
|
|
1189
|
-
time: z.int().positive(),
|
|
1190
|
-
}).buildVoid({
|
|
1191
|
-
input: z.object({}), // optional input schema
|
|
1192
|
-
handler: async ({ options: { emit, isClosed } }) => {
|
|
1193
|
-
while (!isClosed()) {
|
|
1194
|
-
emit("time", Date.now());
|
|
1195
|
-
await setTimeout(1000);
|
|
1196
|
-
}
|
|
1197
|
-
},
|
|
1198
|
-
});
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
If you need more capabilities, such as bidirectional event sending, I have developed an additional websocket operating
|
|
1202
|
-
framework, [Zod Sockets](https://github.com/RobinTail/zod-sockets), which has similar principles and capabilities.
|
|
1203
|
-
|
|
1204
1048
|
# Integration and Documentation
|
|
1205
1049
|
|
|
1206
1050
|
## Zod Plugin
|
|
@@ -1258,7 +1102,6 @@ const yamlString = new Documentation({
|
|
|
1258
1102
|
serverUrl: "https://example.com",
|
|
1259
1103
|
composition: "inline", // optional, or "components" for keeping schemas in a separate dedicated section using refs
|
|
1260
1104
|
// descriptions: { positiveResponse, negativeResponse, requestParameter, requestBody }, // check out these features
|
|
1261
|
-
// numericRange: null, // to disable printing min/max values for z.number() based on JS engine limits
|
|
1262
1105
|
}).getSpecAsYaml();
|
|
1263
1106
|
```
|
|
1264
1107
|
|
|
@@ -1383,6 +1226,150 @@ new Integration({
|
|
|
1383
1226
|
});
|
|
1384
1227
|
```
|
|
1385
1228
|
|
|
1229
|
+
# Special needs
|
|
1230
|
+
|
|
1231
|
+
## Different responses for different status codes
|
|
1232
|
+
|
|
1233
|
+
In some special cases you may want the ResultHandler to respond slightly differently depending on the status code,
|
|
1234
|
+
for example if your API strictly follows REST standards. It may also be necessary to reflect this difference in the
|
|
1235
|
+
generated Documentation. For that purpose, the constructor of `ResultHandler` accepts flexible declaration of possible
|
|
1236
|
+
response schemas and their corresponding status codes.
|
|
1237
|
+
|
|
1238
|
+
```typescript
|
|
1239
|
+
import { ResultHandler } from "express-zod-api";
|
|
1240
|
+
|
|
1241
|
+
new ResultHandler({
|
|
1242
|
+
positive: (data) => ({
|
|
1243
|
+
statusCode: [201, 202], // created or will be created
|
|
1244
|
+
schema: z.object({ status: z.literal("created"), data }),
|
|
1245
|
+
}),
|
|
1246
|
+
negative: [
|
|
1247
|
+
{
|
|
1248
|
+
statusCode: 409, // conflict: entity already exists
|
|
1249
|
+
schema: z.object({ status: z.literal("exists"), id: z.int() }),
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
statusCode: [400, 500], // validation or internal error
|
|
1253
|
+
schema: z.object({ status: z.literal("error"), reason: z.string() }),
|
|
1254
|
+
},
|
|
1255
|
+
],
|
|
1256
|
+
handler: ({ error, response, output }) => {
|
|
1257
|
+
// your implementation here
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
## Array response
|
|
1263
|
+
|
|
1264
|
+
Please avoid doing this in new projects: responding with array is a bad practice keeping your endpoints from evolving
|
|
1265
|
+
in backward compatible way (without making breaking changes). Nevertheless, for the purpose of easier migration of
|
|
1266
|
+
legacy APIs to this framework consider using `arrayResultHandler` or `arrayEndpointsFactory` instead of default ones,
|
|
1267
|
+
or implement your own ones in a similar way.
|
|
1268
|
+
The `arrayResultHandler` expects your endpoint to have `items` property in the `output` object schema. The array
|
|
1269
|
+
assigned to that property is used as the response. This approach also supports examples, as well as documentation and
|
|
1270
|
+
client generation. Check out [the example endpoint](/example/endpoints/list-users.ts) for more details.
|
|
1271
|
+
|
|
1272
|
+
## Accepting raw data
|
|
1273
|
+
|
|
1274
|
+
Some APIs may require an endpoint to be able to accept and process raw data, such as streaming or uploading a binary
|
|
1275
|
+
file as an entire body of request. Use the proprietary `ez.raw()` schema as the input schema of your endpoint.
|
|
1276
|
+
The default parser in this case is `express.raw()`. You can customize it by assigning the `rawParser` option in config.
|
|
1277
|
+
The raw data is placed into `request.body.raw` property, having type `Buffer`.
|
|
1278
|
+
|
|
1279
|
+
```typescript
|
|
1280
|
+
import { defaultEndpointsFactory, ez } from "express-zod-api";
|
|
1281
|
+
|
|
1282
|
+
const rawAcceptingEndpoint = defaultEndpointsFactory.build({
|
|
1283
|
+
method: "post",
|
|
1284
|
+
input: ez.raw({
|
|
1285
|
+
/* the place for additional inputs, like route params, if needed */
|
|
1286
|
+
}),
|
|
1287
|
+
output: z.object({ length: z.int().nonnegative() }),
|
|
1288
|
+
handler: async ({ input: { raw } }) => ({
|
|
1289
|
+
length: raw.length, // raw is Buffer
|
|
1290
|
+
}),
|
|
1291
|
+
});
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
## Profiling
|
|
1295
|
+
|
|
1296
|
+
For debugging and performance testing purposes the framework offers a simple `.profile()` method on the built-in logger.
|
|
1297
|
+
It starts a timer when you call it and measures the duration in adaptive units (from picoseconds to minutes) until you
|
|
1298
|
+
invoke the returned callback. The default severity of those measurements is `debug`.
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
import { createConfig, BuiltinLogger } from "express-zod-api";
|
|
1302
|
+
|
|
1303
|
+
// This enables the .profile() method on built-in logger:
|
|
1304
|
+
declare module "express-zod-api" {
|
|
1305
|
+
interface LoggerOverrides extends BuiltinLogger {}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Inside a handler of Endpoint, Middleware or ResultHandler:
|
|
1309
|
+
const done = logger.profile("expensive operation");
|
|
1310
|
+
doExpensiveOperation();
|
|
1311
|
+
done(); // debug: expensive operation '555 milliseconds'
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
You can also customize the profiler with your own formatter, chosen severity or even performance assessment function:
|
|
1315
|
+
|
|
1316
|
+
```typescript
|
|
1317
|
+
logger.profile({
|
|
1318
|
+
message: "expensive operation",
|
|
1319
|
+
severity: (ms) => (ms > 500 ? "error" : "info"), // assess immediately
|
|
1320
|
+
formatter: (ms) => `${ms.toFixed(2)}ms`, // custom format
|
|
1321
|
+
});
|
|
1322
|
+
doExpensiveOperation();
|
|
1323
|
+
done(); // error: expensive operation '555.55ms'
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
## Graceful shutdown
|
|
1327
|
+
|
|
1328
|
+
You can enable and configure a special request monitoring that, if it receives a signal to terminate a process, will
|
|
1329
|
+
first put the server into a mode that rejects new requests, attempt to complete started requests within the specified
|
|
1330
|
+
time, and then forcefully stop the server and terminate the process.
|
|
1331
|
+
|
|
1332
|
+
```ts
|
|
1333
|
+
import { createConfig } from "express-zod-api";
|
|
1334
|
+
|
|
1335
|
+
createConfig({
|
|
1336
|
+
gracefulShutdown: {
|
|
1337
|
+
timeout: 1000,
|
|
1338
|
+
events: ["SIGINT", "SIGTERM"],
|
|
1339
|
+
beforeExit: /* async */ () => {},
|
|
1340
|
+
},
|
|
1341
|
+
});
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
## Subscriptions
|
|
1345
|
+
|
|
1346
|
+
If you want the user of a client application to be able to subscribe to subsequent updates initiated by the server,
|
|
1347
|
+
consider [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) feature.
|
|
1348
|
+
Client application can subscribe to the event stream using `EventSource` class instance or the
|
|
1349
|
+
[instance of the generated](#generating-a-frontend-client) `Subscription` class. The following example demonstrates
|
|
1350
|
+
the implementation emitting the `time` event each second.
|
|
1351
|
+
|
|
1352
|
+
```typescript
|
|
1353
|
+
import { z } from "zod/v4";
|
|
1354
|
+
import { EventStreamFactory } from "express-zod-api";
|
|
1355
|
+
import { setTimeout } from "node:timers/promises";
|
|
1356
|
+
|
|
1357
|
+
const subscriptionEndpoint = new EventStreamFactory({
|
|
1358
|
+
time: z.int().positive(),
|
|
1359
|
+
}).buildVoid({
|
|
1360
|
+
input: z.object({}), // optional input schema
|
|
1361
|
+
handler: async ({ options: { emit, isClosed } }) => {
|
|
1362
|
+
while (!isClosed()) {
|
|
1363
|
+
emit("time", Date.now());
|
|
1364
|
+
await setTimeout(1000);
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
If you need more capabilities, such as bidirectional event sending, I have developed an additional websocket operating
|
|
1371
|
+
framework, [Zod Sockets](https://github.com/RobinTail/zod-sockets), which has similar principles and capabilities.
|
|
1372
|
+
|
|
1386
1373
|
# Caveats
|
|
1387
1374
|
|
|
1388
1375
|
There are some well-known issues and limitations, or third party bugs that cannot be fixed in the usual way, but you
|