Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Empty response support #2191

Merged
merged 100 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
42573ae
Changing trigger branches.
RobinTail Oct 8, 2024
b911917
Prohibit vulnerable versions of `express` (#2083)
RobinTail Oct 8, 2024
6c8571e
Merge branch 'master' into make-v21
RobinTail Oct 10, 2024
f200136
Cleaning up migration for v21 (#2087)
RobinTail Oct 10, 2024
fb5a786
21.0.0-beta.0
RobinTail Oct 10, 2024
2dd4567
Activating migration test.
RobinTail Oct 10, 2024
4c54b45
Dedicated to Kesaria Abramidze.
RobinTail Oct 10, 2024
fa2bccf
Making HTTP server optional (#2086)
RobinTail Oct 10, 2024
62be668
Merge branch 'master' into make-v21
RobinTail Oct 10, 2024
99fbd9d
Merge branch 'master' into make-v21
RobinTail Oct 15, 2024
024411f
Return generic array of servers (#2100)
RobinTail Oct 15, 2024
f0c9777
Merge branch 'master' into make-v21
RobinTail Oct 15, 2024
e8acf93
Merge branch 'master' into make-v21
RobinTail Oct 15, 2024
54afdb2
Merge branch 'master' into make-v21
RobinTail Oct 16, 2024
43cb643
Merge branch 'master' into make-v21
RobinTail Oct 16, 2024
5ca76bc
Rev: naming of server const in system test.
RobinTail Oct 16, 2024
83ad924
Merge branch 'master' into make-v21
RobinTail Oct 24, 2024
4066257
Merge branch 'master' into make-v21
RobinTail Oct 26, 2024
29ce100
Drop `serializer` prop (#2122)
RobinTail Oct 26, 2024
f280e76
Merge branch 'master' into make-v21
RobinTail Oct 26, 2024
5cd0c2a
Changelog: 21.0.0 draft.
RobinTail Oct 26, 2024
9bbb117
Changelog: migration advice.
RobinTail Oct 27, 2024
953d1cf
Merge branch 'master' into make-v21
RobinTail Nov 1, 2024
2466375
Merge branch 'master' into make-v21
RobinTail Nov 2, 2024
50165f9
Drop `originalError` property on validation errors (#2139)
RobinTail Nov 2, 2024
5f00dc3
Merge branch 'master' into make-v21
RobinTail Nov 4, 2024
9b67d1d
Drop `getStatusCodeFromError()` (#2148)
RobinTail Nov 4, 2024
2bc29f7
Changelog: note on getStatusCodeFromError
RobinTail Nov 4, 2024
02dc007
Merge branch 'master' into make-v21
RobinTail Nov 4, 2024
fcd43b7
rm todo.
RobinTail Nov 5, 2024
ee2b9ec
Merge branch 'master' into make-v21
RobinTail Nov 5, 2024
6c6cbc9
Merge branch 'master' into make-v21
RobinTail Nov 8, 2024
06a77f9
Merge branch 'master' into make-v21
RobinTail Nov 9, 2024
e2286fa
Merge branch 'master' into make-v21
RobinTail Nov 11, 2024
6e868fa
Making `method` and `methods` optional (#2128)
RobinTail Nov 12, 2024
2c492e7
Merge branch 'master' into make-v21
RobinTail Nov 12, 2024
635f478
Security: planning for december.
RobinTail Nov 12, 2024
0317deb
Deprecating v17 (#2162)
RobinTail Nov 12, 2024
0e0159c
21.0.0-beta.2
RobinTail Nov 12, 2024
cd422c6
Merge branch 'master' into make-v21
RobinTail Nov 13, 2024
bc3db93
Provide universal `getLogger()` to `beforeRouting` (#2167)
RobinTail Nov 14, 2024
a228dce
Merge branch 'master' into make-v21
RobinTail Nov 15, 2024
b8f0906
Merge branch 'master' into make-v21
RobinTail Nov 15, 2024
bc8e9eb
Traverse without recursion (#2168)
RobinTail Nov 15, 2024
9f1e0bf
Performance tuning for `onObject` zts producer (#2171)
RobinTail Nov 15, 2024
6d6e330
Revert "Performance tuning for `onObject` zts producer (#2171)"
RobinTail Nov 15, 2024
dfd9d72
Ref, rev: using Object.entries() in traverse instead of toPairs().
RobinTail Nov 15, 2024
b288b3a
Addressing performance findings (#2172)
RobinTail Nov 15, 2024
6ae145e
21.0.0-beta.3
RobinTail Nov 15, 2024
ea41234
Merge branch 'master' into make-v21
RobinTail Nov 15, 2024
0c3e94f
Merge branch 'master' into make-v21
RobinTail Nov 15, 2024
42f30d1
Changelog: giving a sample of new config and new returns.
RobinTail Nov 16, 2024
af5a7bf
Changelog: minor, arrangement.
RobinTail Nov 16, 2024
914f9d0
Changelog: more details on http prop in example.
RobinTail Nov 16, 2024
a65472f
Generalising server type (#2174)
RobinTail Nov 16, 2024
97b2f5a
Revert "Generalising server type (#2174)"
RobinTail Nov 16, 2024
8310daf
Ref: defining servers union type once.
RobinTail Nov 16, 2024
7cf239f
Merge branch 'master' into make-v21
RobinTail Nov 16, 2024
e1c58d5
rm redundant createConfig() in server unit test.
RobinTail Nov 16, 2024
02d1732
Removing redundant methods array in tests. (#2176)
RobinTail Nov 16, 2024
a08a31a
Merge branch 'master' into make-v21
RobinTail Nov 16, 2024
3f7bf45
Changelog: better structure for describing breaking changes of v21.
RobinTail Nov 17, 2024
8bbb336
Universal properties for arrays (#2175)
RobinTail Nov 17, 2024
9589b61
Merge branch 'master' into make-v21
RobinTail Nov 17, 2024
ce0df21
21.0.0-beta.4
RobinTail Nov 17, 2024
f33c4c8
Ref: extracting esQueries in migration.
RobinTail Nov 17, 2024
38bbbd0
Ease `DocumentationError` constructor (#2185)
RobinTail Nov 17, 2024
f5bc953
Merge branch 'master' into make-v21
RobinTail Nov 18, 2024
43eaf2a
Example: no more requirement on methods when using DependsOnMethod
RobinTail Nov 18, 2024
2ea1f65
Readme: shortening obvious statements. (#2187)
RobinTail Nov 18, 2024
c3cb2db
Merge branch 'master' into make-v21
RobinTail Nov 18, 2024
8efed65
Merge branch 'master' into make-v21
RobinTail Nov 18, 2024
01f3a78
rm redundant method in nesting test
RobinTail Nov 18, 2024
2ef3081
Merge branch 'master' into make-v21
RobinTail Nov 18, 2024
d470398
Draft: handling no content responses, allowing z.never().
RobinTail Nov 18, 2024
c765603
Allowing empty array of mime types causing no depictResponse() work.
RobinTail Nov 18, 2024
8aeb90f
Rev: no z.never() producer in zts.
RobinTail Nov 18, 2024
89864cf
Update src/documentation-helpers.ts
RobinTail Nov 18, 2024
6ae3da7
Update src/documentation-helpers.ts
RobinTail Nov 18, 2024
b4a1a79
Producing undefined in case of no content in client types.
RobinTail Nov 18, 2024
1f982c8
Fixes after rebase.
RobinTail Nov 18, 2024
f0a5d81
Fixing the nested route traverse order — FIFO (#2192)
RobinTail Nov 18, 2024
297ca78
Merge branch 'make-v21' into no-content-response
RobinTail Nov 18, 2024
4f6a34b
Updating snapshots after traverse order fixed.
RobinTail Nov 18, 2024
3956848
Ref: simpler condition in depictResponse.
RobinTail Nov 18, 2024
86d476b
Extracting noContent schema into an option.
RobinTail Nov 18, 2024
b3dd047
Content type check by the example client implementation.
RobinTail Nov 19, 2024
f65715a
System tests.
RobinTail Nov 19, 2024
eba57f1
Introducing emptyResponse() public helper.
RobinTail Nov 19, 2024
035f0e5
Drop `Endpoint::getMimeTypes()` (#2193)
RobinTail Nov 19, 2024
705692b
Merge branch 'make-v21' into no-content-response
RobinTail Nov 19, 2024
7773153
REF: null mimeType as identifier of no content.
RobinTail Nov 19, 2024
1e08120
Rev: removing helper.
RobinTail Nov 19, 2024
d9c50b7
Minor: jsdoc.
RobinTail Nov 19, 2024
48cced2
Organize response normalization (#2194)
RobinTail Nov 19, 2024
51f9be5
Merge branch 'make-v21' into no-content-response
RobinTail Nov 19, 2024
ee0386b
Merge branch 'master' into no-content-response
RobinTail Nov 20, 2024
ad7ea06
Changelog: 21.1.0.
RobinTail Nov 20, 2024
178fd23
Readme: listing the feature.
RobinTail Nov 20, 2024
cbb5517
21.1.0-beta.1
RobinTail Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

## Version 21

### v21.1.0

- Featuring empty response support:
- For some REST APIs, empty responses are typical: with status code `204` (No Content) and redirects (302);
- Previously, the framework did not offer a straightforward way to describe such responses, but now there is one;
- The `mimeType` property can now be assigned with `null` in `ResultHandler` definition;
- Both `Documentation` and `Integration` generators ignore such entries so that the `schema` can be `z.never()`:
- The body of such response will not be depicted by `Documentation`;
- The type of such response will be described as `undefined` (configurable) by `Integration`.

```ts
import { z } from "zod";
import {
ResultHandler,
ensureHttpError,
EndpointsFactory,
Integration,
} from "express-zod-api";

const resultHandler = new ResultHandler({
positive: { statusCode: 204, mimeType: null, schema: z.never() },
negative: { statusCode: 404, mimeType: null, schema: z.never() },
handler: ({ error, response }) => {
response.status(error ? ensureHttpError(error).statusCode : 204).end(); // no content
},
});

new Integration({ noContent: z.undefined() }); // undefined is default
```

### v21.0.0

- Minimum supported versions of `express`: 4.21.1 and 5.0.1 (fixed vulnerabilities);
Expand Down
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ Start your API server with I/O schema validation and custom middlewares in minut
3. [Route path params](#route-path-params)
4. [Multiple schemas for one route](#multiple-schemas-for-one-route)
5. [Response customization](#response-customization)
6. [Error handling](#error-handling)
7. [Production mode](#production-mode)
8. [Non-object response](#non-object-response) including file downloads
9. [File uploads](#file-uploads)
10. [Serving static files](#serving-static-files)
11. [Connect to your own express app](#connect-to-your-own-express-app)
12. [Testing endpoints](#testing-endpoints)
13. [Testing middlewares](#testing-middlewares)
6. [Empty response](#empty-response)
7. [Error handling](#error-handling)
8. [Production mode](#production-mode)
9. [Non-object response](#non-object-response) including file downloads
10. [File uploads](#file-uploads)
11. [Serving static files](#serving-static-files)
12. [Connect to your own express app](#connect-to-your-own-express-app)
13. [Testing endpoints](#testing-endpoints)
14. [Testing middlewares](#testing-middlewares)
6. [Special needs](#special-needs)
1. [Different responses for different status codes](#different-responses-for-different-status-codes)
2. [Array response](#array-response) for migrating legacy APIs
Expand Down Expand Up @@ -843,6 +844,18 @@ import { EndpointsFactory } from "express-zod-api";
const endpointsFactory = new EndpointsFactory(yourResultHandler);
```

## Empty response

For some REST APIs, empty responses are typical: with status code `204` (No Content) and redirects (302). In order to
describe it set the `mimeType` to `null` and `schema` to `z.never()`:

```typescript
const resultHandler = new ResultHandler({
positive: { statusCode: 204, mimeType: null, schema: z.never() },
negative: { statusCode: 404, mimeType: null, schema: z.never() },
});
```

## Error handling

`ResultHandler` is designed to be the entity responsible for centralized error handling. By default, that center is
Expand Down
22 changes: 22 additions & 0 deletions example/endpoints/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import createHttpError from "http-errors";
import assert from "node:assert/strict";
import { z } from "zod";
import { noContentFactory } from "../factories";

/** @desc The endpoint demonstrates no content response established by its factory */
export const deleteUserEndpoint = noContentFactory.build({
method: "delete",
tag: "users",
input: z.object({
id: z
.string()
.regex(/\d+/)
.transform((id) => parseInt(id, 10))
.describe("numeric string"),
}),
output: z.object({}),
handler: async ({ input: { id } }) => {
assert(id <= 100, createHttpError(404, "User not found"));
return {};
},
});
17 changes: 14 additions & 3 deletions example/example.client.ts
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ type GetV1UserRetrieveResponse =
};
};

type DeleteV1UserIdRemoveInput = {
/** numeric string */
id: string;
};

type DeleteV1UserIdRemoveResponse = undefined;

type PatchV1UserIdInput = {
key: string;
id: string;
Expand Down Expand Up @@ -131,6 +138,7 @@ type PostV1AvatarRawResponse =

export type Path =
| "/v1/user/retrieve"
| "/v1/user/:id/remove"
| "/v1/user/:id"
| "/v1/user/create"
| "/v1/user/list"
Expand All @@ -145,6 +153,7 @@ export type MethodPath = `${Method} ${Path}`;

export interface Input extends Record<MethodPath, any> {
"get /v1/user/retrieve": GetV1UserRetrieveInput;
"delete /v1/user/:id/remove": DeleteV1UserIdRemoveInput;
"patch /v1/user/:id": PatchV1UserIdInput;
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
"post /v1/user/create": PostV1UserCreateInput;
"get /v1/user/list": GetV1UserListInput;
Expand All @@ -156,6 +165,7 @@ export interface Input extends Record<MethodPath, any> {

export interface Response extends Record<MethodPath, any> {
"get /v1/user/retrieve": GetV1UserRetrieveResponse;
"delete /v1/user/:id/remove": DeleteV1UserIdRemoveResponse;
"patch /v1/user/:id": PatchV1UserIdResponse;
"post /v1/user/create": PostV1UserCreateResponse;
"get /v1/user/list": GetV1UserListResponse;
Expand All @@ -176,6 +186,7 @@ export const jsonEndpoints = {

export const endpointTags = {
"get /v1/user/retrieve": ["users"],
"delete /v1/user/:id/remove": ["users"],
"patch /v1/user/:id": ["users"],
"post /v1/user/create": ["users"],
"get /v1/user/list": ["users"],
Expand Down Expand Up @@ -228,9 +239,9 @@ export const exampleImplementation: Implementation = async (
headers: hasBody ? { "Content-Type": "application/json" } : undefined,
body: hasBody ? JSON.stringify(params) : undefined,
});
const isJSON = response.headers
.get("content-type")
?.startsWith("application/json");
const contentType = response.headers.get("content-type");
if (!contentType) return;
const isJSON = contentType.startsWith("application/json");
return response[isJSON ? "json" : "text"]();
};
const client = new ExpressZodAPIClient(exampleImplementation);
Expand Down
21 changes: 20 additions & 1 deletion example/example.documentation.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Example API
version: 21.0.0
version: 21.1.0-beta.1
paths:
/v1/user/retrieve:
get:
Expand Down Expand Up @@ -85,6 +85,25 @@ paths:
status: error
error:
message: Sample error message
/v1/user/{id}/remove:
delete:
operationId: DeleteV1UserIdRemove
tags:
- users
parameters:
- name: id
in: path
required: true
description: numeric string
schema:
type: string
pattern: \d+
description: numeric string
responses:
"204":
description: DELETE /v1/user/:id/remove Positive response
"404":
description: DELETE /v1/user/:id/remove Negative response
/v1/user/{id}:
patch:
operationId: PatchV1UserId
Expand Down
12 changes: 12 additions & 0 deletions example/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,15 @@ export const statusDependingFactory = new EndpointsFactory({
},
}),
});

/** @desc This factory demonstrates response without body, such as 204 No Content */
export const noContentFactory = new EndpointsFactory({
config,
resultHandler: new ResultHandler({
positive: { statusCode: 204, mimeType: null, schema: z.never() },
negative: { statusCode: 404, mimeType: null, schema: z.never() },
handler: ({ error, response }) => {
response.status(error ? ensureHttpError(error).statusCode : 204).end(); // no content
},
}),
});
3 changes: 3 additions & 0 deletions example/routing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DependsOnMethod, Routing, ServeStatic } from "../src";
import { rawAcceptingEndpoint } from "./endpoints/accept-raw";
import { createUserEndpoint } from "./endpoints/create-user";
import { deleteUserEndpoint } from "./endpoints/delete-user";
import { listUsersEndpoint } from "./endpoints/list-users";
import { uploadAvatarEndpoint } from "./endpoints/upload-avatar";
import { retrieveUserEndpoint } from "./endpoints/retrieve-user";
Expand All @@ -17,6 +18,8 @@ export const routing: Routing = {
// syntax 2: methods are defined within the route (id is the route path param by the way)
":id": new DependsOnMethod({
patch: updateUserEndpoint, // demonstrates authentication
}).nest({
remove: deleteUserEndpoint, // nested path: /v1/user/:id/remove
}),
// demonstrates different response schemas depending on status code
create: createUserEndpoint,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "express-zod-api",
"version": "21.0.0",
"version": "21.1.0-beta.1",
"description": "A Typescript framework to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.",
"license": "MIT",
"repository": {
Expand Down
9 changes: 6 additions & 3 deletions src/api-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ export interface ApiResponse<S extends z.ZodTypeAny> {
schema: S;
/** @default 200 for a positive and 400 for a negative response */
statusCode?: number | [number, ...number[]];
/** @default "application/json" */
mimeType?: string | [string, ...string[]];
/**
* @example null is for no content, such as 204 and 302
* @default "application/json"
* */
mimeType?: string | [string, ...string[]] | null;
/** @deprecated use statusCode */
statusCodes?: never;
/** @deprecated use mimeType */
Expand All @@ -27,5 +30,5 @@ export interface ApiResponse<S extends z.ZodTypeAny> {
export interface NormalizedResponse {
schema: z.ZodTypeAny;
statusCodes: [number, ...number[]];
mimeTypes: [string, ...string[]];
mimeTypes: [string, ...string[]] | null;
}
2 changes: 1 addition & 1 deletion src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class Diagnostics {
}
for (const variant of ["positive", "negative"] as const) {
for (const { mimeTypes, schema } of endpoint.getResponses(variant)) {
if (mimeTypes.includes(contentTypes.json)) {
if (mimeTypes?.includes(contentTypes.json)) {
try {
assertJsonCompatible(schema, "out");
} catch (reason) {
Expand Down
3 changes: 2 additions & 1 deletion src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,11 +785,12 @@ export const depictResponse = ({
hasMultipleStatusCodes ? statusCode : ""
}`.trim(),
}: ReqResHandlingProps<z.ZodTypeAny> & {
mimeTypes: ReadonlyArray<string>;
mimeTypes: ReadonlyArray<string> | null;
variant: ResponseVariant;
statusCode: number;
hasMultipleStatusCodes: boolean;
}): ResponseObject => {
if (!mimeTypes) return { description };
const depictedSchema = excludeExamplesFromDepiction(
walkSchema(schema, {
rules: { ...brandHandling, ...depicters },
Expand Down
Loading