express-zod-api 21.0.0-beta.4 → 21.0.0-beta.6
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 +36 -1
- package/README.md +52 -67
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +39 -35
- package/dist/index.d.ts +39 -35
- package/dist/index.js +11 -11
- package/migration/index.cjs +1 -1
- package/migration/index.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
- The `serializer` property of `Documentation` and `Integration` constructor argument removed;
|
|
22
22
|
- The `originalError` property of `InputValidationError` and `OutputValidationError` removed (use `cause` instead);
|
|
23
23
|
- The `getStatusCodeFromError()` method removed (use the `ensureHttpError().statusCode` instead);
|
|
24
|
-
- The `Endpoint::getMethods()` method may now return `undefined`;
|
|
25
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;
|
|
26
26
|
- Public properties `pairs`, `firstEndpoint` and `siblingMethods` of `DependsOnMethod` replaced with `entries`.
|
|
27
27
|
- Consider the automated migration using the built-in ESLint rule.
|
|
28
28
|
|
|
@@ -56,6 +56,41 @@ const { servers } = await createServer(config, {});
|
|
|
56
56
|
|
|
57
57
|
## Version 20
|
|
58
58
|
|
|
59
|
+
### v20.22.1
|
|
60
|
+
|
|
61
|
+
- Avoids startup logo distortion when the terminal is too narrow;
|
|
62
|
+
- Self-diagnosis for potential problems disabled in production mode to ensure faster startup:
|
|
63
|
+
- Warning about potentially unserializable schema for JSON operating endpoints was introduced in v20.15.0.
|
|
64
|
+
|
|
65
|
+
### v20.22.0
|
|
66
|
+
|
|
67
|
+
- Featuring a helper to describe nested Routing for already assigned routes:
|
|
68
|
+
- Suppose you want to describe `Routing` for both `/v1/path` and `/v1/path/subpath` routes having Endpoints attached;
|
|
69
|
+
- Previously, an empty path segment was proposed for that purpose, but there is more elegant and readable way now;
|
|
70
|
+
- The `.nest()` method is available both on `Endpoint` and `DependsOnMethod` instances:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { Routing } from "express-zod-api";
|
|
74
|
+
|
|
75
|
+
// Describing routes /v1/path and /v1/path/subpath both having endpoints assigned:
|
|
76
|
+
const before: Routing = {
|
|
77
|
+
v1: {
|
|
78
|
+
path: {
|
|
79
|
+
"": endpointA,
|
|
80
|
+
subpath: endpointB,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const after: Routing = {
|
|
86
|
+
v1: {
|
|
87
|
+
path: endpointA.nest({
|
|
88
|
+
subpath: endpointB,
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
59
94
|
### v20.21.2
|
|
60
95
|
|
|
61
96
|
- Fixed the example implementation in the generated client for endpoints using path params:
|
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. [
|
|
36
|
-
3. [
|
|
37
|
-
4. [
|
|
38
|
-
5. [
|
|
39
|
-
6. [
|
|
40
|
-
7. [
|
|
41
|
-
8. [
|
|
42
|
-
9. [
|
|
43
|
-
10. [
|
|
44
|
-
11. [
|
|
45
|
-
12. [Testing
|
|
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
|
|
@@ -287,12 +288,9 @@ const authMiddleware = new Middleware({
|
|
|
287
288
|
handler: async ({ input: { key }, request, logger }) => {
|
|
288
289
|
logger.debug("Checking the key and token");
|
|
289
290
|
const user = await db.Users.findOne({ key });
|
|
290
|
-
if (!user)
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
if (request.headers.token !== user.token) {
|
|
291
|
+
if (!user) throw createHttpError(401, "Invalid key");
|
|
292
|
+
if (request.headers.token !== user.token)
|
|
294
293
|
throw createHttpError(401, "Invalid token");
|
|
295
|
-
}
|
|
296
294
|
return { user }; // provides endpoints with options.user
|
|
297
295
|
},
|
|
298
296
|
});
|
|
@@ -304,10 +302,9 @@ By using `.addMiddleware()` method before `.build()` you can connect it to the e
|
|
|
304
302
|
const yourEndpoint = defaultEndpointsFactory
|
|
305
303
|
.addMiddleware(authMiddleware)
|
|
306
304
|
.build({
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
},
|
|
305
|
+
handler: async ({ options: { user } }) => {
|
|
306
|
+
// user is the one returned by authMiddleware
|
|
307
|
+
}, // ...
|
|
311
308
|
});
|
|
312
309
|
```
|
|
313
310
|
|
|
@@ -321,7 +318,7 @@ import { defaultEndpointsFactory } from "express-zod-api";
|
|
|
321
318
|
const factory = defaultEndpointsFactory
|
|
322
319
|
.addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:
|
|
323
320
|
.addMiddleware({
|
|
324
|
-
handler: async ({ options: { user } }) => ({}), //
|
|
321
|
+
handler: async ({ options: { user } }) => ({}), // user from authMiddleware
|
|
325
322
|
});
|
|
326
323
|
```
|
|
327
324
|
|
|
@@ -535,18 +532,14 @@ const updateUserEndpoint = defaultEndpointsFactory.build({
|
|
|
535
532
|
method: "post",
|
|
536
533
|
input: z.object({
|
|
537
534
|
userId: z.string(),
|
|
538
|
-
birthday: ez.dateIn(), // string -> Date
|
|
535
|
+
birthday: ez.dateIn(), // string -> Date in handler
|
|
539
536
|
}),
|
|
540
537
|
output: z.object({
|
|
541
|
-
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
|
|
542
542
|
}),
|
|
543
|
-
handler: async ({ input }) => {
|
|
544
|
-
// input.birthday is Date
|
|
545
|
-
return {
|
|
546
|
-
// transmitted as "2022-01-22T00:00:00.000Z"
|
|
547
|
-
createdAt: new Date("2022-01-22"),
|
|
548
|
-
};
|
|
549
|
-
},
|
|
550
543
|
});
|
|
551
544
|
```
|
|
552
545
|
|
|
@@ -564,7 +557,6 @@ That function has several parameters and can be asynchronous.
|
|
|
564
557
|
import { createConfig } from "express-zod-api";
|
|
565
558
|
|
|
566
559
|
const config = createConfig({
|
|
567
|
-
// ... other options
|
|
568
560
|
cors: ({ defaultHeaders, request, endpoint, logger }) => ({
|
|
569
561
|
...defaultHeaders,
|
|
570
562
|
"Access-Control-Max-Age": "5000",
|
|
@@ -590,8 +582,7 @@ const config = createConfig({
|
|
|
590
582
|
key: fs.readFileSync("privkey.pem", "utf-8"),
|
|
591
583
|
},
|
|
592
584
|
listen: 443, // port, UNIX socket or options
|
|
593
|
-
},
|
|
594
|
-
// ... cors, logger, etc
|
|
585
|
+
}, // ... cors, logger, etc
|
|
595
586
|
});
|
|
596
587
|
|
|
597
588
|
// 'await' is only needed if you're going to use the returned entities.
|
|
@@ -718,14 +709,13 @@ In order to receive a compressed response the client should include the followin
|
|
|
718
709
|
|
|
719
710
|
You can customize the list of `request` properties that are combined into `input` that is being validated and available
|
|
720
711
|
to your endpoints and middlewares. The order here matters: each next item in the array has a higher priority than its
|
|
721
|
-
previous sibling.
|
|
712
|
+
previous sibling. The following arrangement is default:
|
|
722
713
|
|
|
723
714
|
```typescript
|
|
724
715
|
import { createConfig } from "express-zod-api";
|
|
725
716
|
|
|
726
717
|
createConfig({
|
|
727
718
|
inputSources: {
|
|
728
|
-
// the defaults are:
|
|
729
719
|
get: ["query", "params"],
|
|
730
720
|
post: ["body", "params", "files"],
|
|
731
721
|
put: ["body", "params"],
|
|
@@ -735,21 +725,32 @@ createConfig({
|
|
|
735
725
|
});
|
|
736
726
|
```
|
|
737
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
|
+
|
|
738
744
|
## Route path params
|
|
739
745
|
|
|
740
|
-
You can
|
|
746
|
+
You can assign your Endpoint to a route like `/v1/user/:id` where `:id` is the path parameter:
|
|
741
747
|
|
|
742
748
|
```typescript
|
|
743
749
|
import { Routing } from "express-zod-api";
|
|
744
750
|
|
|
745
751
|
const routing: Routing = {
|
|
746
752
|
v1: {
|
|
747
|
-
user: {
|
|
748
|
-
// route path /v1/user/:id, where :id is the path param
|
|
749
|
-
":id": getUserEndpoint,
|
|
750
|
-
// use the empty string to represent /v1/user if needed:
|
|
751
|
-
// "": listAllUsersEndpoint,
|
|
752
|
-
},
|
|
753
|
+
user: { ":id": getUserEndpoint },
|
|
753
754
|
},
|
|
754
755
|
};
|
|
755
756
|
```
|
|
@@ -764,12 +765,8 @@ const getUserEndpoint = endpointsFactory.build({
|
|
|
764
765
|
// other inputs (in query):
|
|
765
766
|
withExtendedInformation: z.boolean().optional(),
|
|
766
767
|
}),
|
|
767
|
-
output: z.object({
|
|
768
|
-
|
|
769
|
-
}),
|
|
770
|
-
handler: async ({ input: { id } }) => {
|
|
771
|
-
// id is the route path param, number
|
|
772
|
-
},
|
|
768
|
+
output: z.object({}),
|
|
769
|
+
handler: async ({ input: { id } }) => ({}), // id is number,
|
|
773
770
|
});
|
|
774
771
|
```
|
|
775
772
|
|
|
@@ -873,6 +870,7 @@ origins of errors that could happen in runtime and be handled the following way:
|
|
|
873
870
|
Consider enabling production mode by setting `NODE_ENV` environment variable to `production` for your deployment:
|
|
874
871
|
|
|
875
872
|
- Express activates some [performance optimizations](https://expressjs.com/en/advanced/best-practice-performance.html);
|
|
873
|
+
- Self-diagnosis for potential problems is disabled to ensure faster startup;
|
|
876
874
|
- The `defaultResultHandler`, `defaultEndpointsFactory` and `LastResortHandler` generalize server-side error messages
|
|
877
875
|
in negative responses in order to improve the security of your API by not disclosing the exact causes of errors:
|
|
878
876
|
- Throwing errors that have or imply `5XX` status codes become just `Internal Server Error` message in response;
|
|
@@ -905,17 +903,12 @@ const fileStreamingEndpointsFactory = new EndpointsFactory(
|
|
|
905
903
|
positive: { schema: ez.file("buffer"), mimeType: "image/*" },
|
|
906
904
|
negative: { schema: z.string(), mimeType: "text/plain" },
|
|
907
905
|
handler: ({ response, error, output }) => {
|
|
908
|
-
if (error)
|
|
909
|
-
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
if ("filename" in output) {
|
|
906
|
+
if (error) return void response.status(400).send(error.message);
|
|
907
|
+
if ("filename" in output)
|
|
913
908
|
fs.createReadStream(output.filename).pipe(
|
|
914
909
|
response.type(output.filename),
|
|
915
910
|
);
|
|
916
|
-
|
|
917
|
-
response.status(400).send("Filename is missing");
|
|
918
|
-
}
|
|
911
|
+
else response.status(400).send("Filename is missing");
|
|
919
912
|
},
|
|
920
913
|
}),
|
|
921
914
|
);
|
|
@@ -949,9 +942,7 @@ const config = createConfig({
|
|
|
949
942
|
limits: { fileSize: 51200 }, // 50 KB
|
|
950
943
|
limitError: createHttpError(413, "The file is too large"), // handled by errorHandler in config
|
|
951
944
|
beforeUpload: ({ request, logger }) => {
|
|
952
|
-
if (!canUpload(request))
|
|
953
|
-
throw createHttpError(403, "Not authorized");
|
|
954
|
-
}
|
|
945
|
+
if (!canUpload(request)) throw createHttpError(403, "Not authorized");
|
|
955
946
|
},
|
|
956
947
|
},
|
|
957
948
|
});
|
|
@@ -1284,9 +1275,7 @@ const exampleEndpoint = defaultEndpointsFactory.build({
|
|
|
1284
1275
|
.object({
|
|
1285
1276
|
id: z.number().describe("the ID of the user"),
|
|
1286
1277
|
})
|
|
1287
|
-
.example({
|
|
1288
|
-
id: 123,
|
|
1289
|
-
}),
|
|
1278
|
+
.example({ id: 123 }),
|
|
1290
1279
|
// ..., similarly for output and middlewares
|
|
1291
1280
|
});
|
|
1292
1281
|
```
|
|
@@ -1309,9 +1298,8 @@ import {
|
|
|
1309
1298
|
} from "express-zod-api";
|
|
1310
1299
|
|
|
1311
1300
|
const config = createConfig({
|
|
1312
|
-
// ..., use the simple or the advanced syntax:
|
|
1313
1301
|
tags: {
|
|
1314
|
-
users: "Everything about the users",
|
|
1302
|
+
users: "Everything about the users", // or advanced syntax:
|
|
1315
1303
|
files: {
|
|
1316
1304
|
description: "Everything about the files processing",
|
|
1317
1305
|
url: "https://example.com",
|
|
@@ -1326,7 +1314,6 @@ const taggedEndpointsFactory = new EndpointsFactory({
|
|
|
1326
1314
|
});
|
|
1327
1315
|
|
|
1328
1316
|
const exampleEndpoint = taggedEndpointsFactory.build({
|
|
1329
|
-
// ...
|
|
1330
1317
|
tag: "users", // or array ["users", "files"]
|
|
1331
1318
|
});
|
|
1332
1319
|
```
|
|
@@ -1365,12 +1352,10 @@ const ruleForClient: Producer = (
|
|
|
1365
1352
|
) => ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
1366
1353
|
|
|
1367
1354
|
new Documentation({
|
|
1368
|
-
/* config, routing, title, version */
|
|
1369
1355
|
brandHandling: { [myBrand]: ruleForDocs },
|
|
1370
1356
|
});
|
|
1371
1357
|
|
|
1372
1358
|
new Integration({
|
|
1373
|
-
/* routing */
|
|
1374
1359
|
brandHandling: { [myBrand]: ruleForClient },
|
|
1375
1360
|
});
|
|
1376
1361
|
```
|