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 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.1, BUT imports MUST be from `zod/v4`;
9
- - Explanation of the versioning strategy: https://github.com/colinhacks/zod/issues/4371;
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
- - Generating Documentation is mostly delegated to Zod 4 `z.toJSONSchema()`:
25
- - The basic depiction of each schema is now natively performed by Zod 4;
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
- - The `optionalPropStyle` option removed from `Integration` class constructor:
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
- - Reasoning: https://x.com/colinhacks/status/1919292504861491252;
33
- - `z.any()` and `z.unknown()` are not optional, details: https://v4.zod.dev/v4/changelog#changes-zunknown-optionality.
34
- - The argument of `ResultHandler::handler` is now discriminated: either `output` or `error` is null, not both;
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
- - The `ez.file()` schema removed: use `z.string()`, `z.base64()` or the new `ez.buffer()` instead;
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. [Customizing logger](#customizing-logger)
31
- 12. [Child logger](#child-logger)
32
- 13. [Profiling](#profiling)
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. [Special needs](#special-needs)
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. _See all available options
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
- listen: 8090, // port, UNIX socket or options
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
- The endpoint responds with "Hello, World" or "Hello, {name}" if the name is supplied within `GET` request payload.
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
- It's _not optional_ to draw your attention to making the appropriate decision, however, it's enabled in the
606
- [Quick start example](#set-up-config) above, assuming that in most cases you will want to enable this feature.
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