express-zod-api
Version:
A Typescript framework to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.
1,275 lines (1,021 loc) • 186 kB
Markdown
# Changelog
## Version 22
### v22.13.2
- Fixed inconsistency between the actual catcher behavior and the error handling documentation:
- Removed conversion of non-`HttpError`s to `BadRequest` before passing them to `errorHandler`;
- A `ResultHandler` configured as `errorHandler` is responsible to handling all errors and responding accordingly.
- The default `errorHandler` is `defaultResultHandler`:
- Using `ensureHttpError()` it coverts non-`HttpError`s to `InternalServerError` and responds with status code `500`;
- The issue has occurred since [v19.0.0](#v1900).
### v22.13.1
- Fixed: the output type of the `ez.raw()` schema (without an argument) was missing the `raw` property (since v19.0.0).
### v22.13.0
- Ability to configure and disable access logging:
- New config option: `accessLogger` — the function for producing access logs;
- The default value is the function writing messages similar to `GET: /v1/path` having `debug` severity;
- The option can be assigned with `null` to disable writing of access logs;
- Thanks to the contributions of [@gmorgen1](https://github.com/gmorgen1) and [@crgeary](https://github.com/crgeary);
- [@danmichaelo](https://github.com/danmichaelo) fixed a broken link in the Security policy;
- Added JSDoc for several types involved into creating Middlewares and producing Endpoints.
```ts
import { createConfig } from "express-zod-api";
const config = createConfig({
accessLogger: (request, logger) => logger.info(request.path), // or null to disable
});
```
### v22.12.0
- Featuring HTML forms support (URL Encoded request body):
- Introducing the new proprietary schema `ez.form()` accepting an object shape or a custom `z.object()` schema;
- Introducing the new config option `formParser` having `express.urlencoded()` as the default value;
- Requests to Endpoints having `input` schema assigned with `ez.form()` are parsed using `formParser`;
- Exception: requests to Endpoints having `ez.upload()` within `ez.form()` are still parsed by `express-fileupload`;
- The lack of this feature was reported by [@james10424](https://github.com/james10424).
```ts
import { defaultEndpointsFactory, ez } from "express-zod-api";
import { z } from "zod";
// The request content type should be "application/x-www-form-urlencoded"
export const submitFeedbackEndpoint = defaultEndpointsFactory.build({
method: "post",
input: ez.form({
name: z.string().min(1),
email: z.string().email(),
message: z.string().min(1),
}),
});
```
### v22.11.2
- Fixed: allow future versions of Express 5:
- Incorrect condition for the peer dependency was introduced in v21.0.0.
```diff
- "express": "^4.21.1 || 5.0.1",
+ "express": "^4.21.1 || ^5.0.1",
```
### v22.11.1
- Simplified the type of `requestMock` returned from `testEndpoint` and `testMiddleware`.
### v22.11.0
- Featuring an ability to configure the numeric range of the generated Documentation:
- The new property `numericRange` on the `Documentation::constructor()` argument provides the way to specify
acceptable limits of `z.number()` and `z.number().int()` that your API can handle;
- Possible values: `{ integer: [number, number], float: [number, number] } | null`;
- Those numbers are used to depict min/max values for `z.number()` schema without limiting refinements;
- The default value is the limits of the JavaScript engine:
- for integers: `[ Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER ]`;
- for floats: `[ -Number.MAX_VALUE, Number.MAX_VALUE ]`;
- The `null` value disables the feature (no min/max values printed unless defined explicitly by the schema);
- This can be useful when a third party tool is having issues to process the generated Documentation;
- Such an issue was reported by [@APTy](https://github.com/APTy) for the Java-based tool:
[openapi-generator](https://github.com/OpenAPITools/openapi-generator);
- Examples of representation of numerical schemes depending on this setting:
```yaml
- schema: z.number()
numericRange: undefined
depicted:
type: number
format: double
minimum: -1.7976931348623157e+308 # -Number.MAX_VALUE
maximum: 1.7976931348623157e+308 # Number.MAX_VALUE
- schema: z.number()
numericRange: null
depicted:
type: number
format: double
- schema: z.number().int()
numericRange: undefined
depicted:
type: integer
format: int64
minimum: -9007199254740991 # Number.MIN_SAFE_INTEGER
maximum: 9007199254740991 # Number.MAX_SAFE_INTEGER
- schema: z.number().int()
numericRange: null
depicted:
type: integer
format: int64
- schema: z.number().int().nonnegative().max(100)
numericRange: undefined
depicted:
type: integer
format: int64
exclusiveMinimum: 0 # explicitly defined by .nonnegative()
maximum: 100 # explicitly defined by .max()
```
### v22.10.1
- Fixed catching errors in a custom `ResultHandler` used as `errorHandler` for Not Found routes having async `handler`.
```ts
// reproduction
import { createConfig, ResultHandler } from "express-zod-api";
createConfig({
errorHandler: new ResultHandler({
// rejected promise was not awaited:
handler: async () => {
throw new Error(
"You should not do it. But if you do, we've got LastResortHandler to catch it.",
);
},
}),
});
```
### v22.10.0
- Featuring required request bodies in the generated Documentation:
- This version sets the `required` property to `requestBody` when:
- It contains the required properties on it;
- Or it's based on `ez.raw()` (proprietary schema);
- The presence of `requestBody` depends on the Endpoint method(s) and the configuration of `inputSources`;
- The lack of the property was reported by [@LufyCZ](https://github.com/LufyCZ).
### v22.9.1
- Minor refactoring and optimizations.
### v22.9.0
- Featuring Deprecations:
- You can deprecate all usage of an `Endpoint` using `EndpointsFactory::build({ deprecated: true })`;
- You can deprecate a route using the assigned `Endpoint::deprecated()` or `DependsOnMethod::deprecated()`;
- You can deprecate a schema using `ZodType::deprecated()`;
- All `.deprecated()` methods are immutable — they create a new copy of the subject;
- Deprecated schemas and endpoints are reflected in the generated `Documentation` and `Integration`;
- The feature suggested by [@mlms13](https://github.com/mlms13).
```ts
import { Routing, DependsOnMethod } from "express-zod-api";
import { z } from "zod";
const someEndpoint = factory.build({
deprecated: true, // deprecates all routes the endpoint assigned to
input: z.object({
prop: z.string().deprecated(), // deprecates the property or a path parameter
}),
});
const routing: Routing = {
v1: oldEndpoint.deprecated(), // deprecates the /v1 path
v2: new DependsOnMethod({ get: oldEndpoint }).deprecated(), // deprecates the /v2 path
v3: someEndpoint, // the path is assigned with initially deprecated endpoint (also deprecated)
};
```
### v22.8.0
- Feature: warning about the endpoint input scheme ignoring the parameters of the route to which it is assigned:
- There is a technological gap between routing and endpoints, which at the same time allows an endpoint to be reused
across multiple routes. Therefore, there are no constraints between the route parameters and the `input` schema;
- This version introduces checking for such discrepancies:
- non-use of the path parameter or,
- a mistake in manually entering its name;
- The warning is displayed when the application is launched and NOT in production mode.
```ts
const updateUserEndpoint = factory.build({
method: "patch",
input: z.object({
id: z.string(), // implies path parameter "id"
}),
});
const routing: Routing = {
v1: {
user: {
":username": updateUserEndpoint, // path parameter is "username" instead of "id"
},
},
};
```
```shell
warn: The input schema of the endpoint is most likely missing the parameter of the path it is assigned to.
{ method: 'patch', path: '/v1/user/:username', param: 'username' }
```
### v22.7.0
- Technical release in connection with the implementation of workspaces into the project architecture.
### v22.6.0
- Feature: pulling examples up from the object schema properties:
- When describing I/O schemas for generating `Documentation` the examples used to work properly only when assigned to
the top level (`z.object().example()`), especially complex scenarios involving path parameters and middlewares;
- This version supports examples assigned to the individual properties on the I/O object schemas;
- It makes the syntax more readable and fixes the issue when example is only set for a path parameter.
```ts
const before = factory.build({
input: z
.object({
key: z.string(),
})
.example({
key: "1234-5678-90",
}),
});
const after = factory.build({
input: z.object({
key: z.string().example("1234-5678-90"),
}),
});
```
### v22.5.0
- Feature: `defaultResultHandler` sets headers from `HttpError`:
- If you `throw createHttpError(400, "message", { headers })` those `headers` go to the negative response.
- Feature: Ability to respond with status code `405` (Method not allowed) to requests having wrong method:
- Previously, in all cases where the method and route combination was not defined, the response had status code `404`;
- For situations where a known route does not support the method being used, there is a more appropriate code `405`:
- See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 for details;
- You can activate this feature by setting the new `wrongMethodBehavior` config option `405` (default: `404`).
```ts
import { createConfig } from "express-zod-api";
createConfig({ wrongMethodBehavior: 405 });
```
### v22.4.2
- Excluded 41 response-only headers from the list of well-known ones used to depict request params in Documentation.
### v22.4.1
- Fixed a bug that could lead to duplicate properties in generated client types:
- If the middleware and/or endpoint schemas had the same property, it was duplicated by Integration.
- The issue was introduced in [v20.15.3](#v20153) and reported by [@bobgubko](https://github.com/bobgubko).
```ts
// reproduction
factory
.addMiddleware({
input: z.object({ query: z.string() }), // ...
})
.build({
input: z.object({ query: z.string() }), // ...
});
```
```ts
type Before = {
query: string;
query: string; // <— bug #2352
};
type After = {
query: string;
};
```
### v22.4.0
- Feat: ability to supply extra data to a custom implementation of the generated client:
- You can instantiate the client class with an implementation accepting an optional context of your choice;
- The public `.provide()` method can now accept an additional argument having the type of that context;
- The problem on missing such ability was reported by [@LucWag](https://github.com/LucWag).
```ts
import { Client, Implementation } from "./generated-client.ts";
interface MyCtx {
extraKey: string;
}
const implementation: Implementation<MyCtx> = async (
method,
path,
params,
ctx, // ctx is optional MyCtx
) => {};
const client = new Client(implementation);
client.provide("get /v1/user/retrieve", { id: "10" }, { extraKey: "123456" });
```
### v22.3.1
- Fixed issue on emitting server-sent events (SSE), introduced in v21.5.0:
- Emitting SSE failed due to internal error `flush is not a function` having `compression` disabled in config;
- The `.flush()` method of `response` is a feature of `compression` (optional peer dependency);
- It is required to call the method when `compression` is enabled;
- This version fixes the issue by calling the method conditionally;
- This bug was reported by [@bobgubko](https://github.com/bobgubko).
### v22.3.0
- Feat: `Subscription` class for consuming Server-sent events:
- The `Integration` can now also generate a frontend helper class `Subscription` to ease SSE support;
- The new class establishes an `EventSource` instance and exposes it as the public `source` property;
- The class also provides the public `on` method for your typed listeners;
- You can configure the generated class name using `subscriptionClassName` option (default: `Subscription`);
- The feature is only applicable to the `variant` option set to `client` (default).
```ts
import { Subscription } from "./client.ts"; // the generated file
new Subscription("get /v1/events/stream", {}).on("time", (time) => {});
```
### v22.2.0
- Feat: detecting headers from `Middleware::security` declarations:
- When `headers` are enabled within `inputSources` of config, the `Documentation` generator can now identify them
among other inputs additionally by using the security declarations of middlewares attached to an `Endpoint`;
- This approach enables handling of custom headers without `x-` prefix.
```ts
const authMiddleware = new Middleware({
security: { type: "header", name: "token" },
});
```
### v22.1.1
- This version contains an important technical simplification of routines related to processing of `security`
declarations of the used `Middleware` when generating API `Documentation`.
- No changes to the operation are expected. This refactoring is required for a feature that will be released later.
### v22.1.0
- Feat: ability to configure the generated client class name:
- New option `clientClassName` for `Integration::constructor()` argument, default: `Client`.
- Feat: default implementation for the generated client:
- The argument of the generated client class constructor became optional;
- The `Implementation` previously suggested as an example (using `fetch`) became the one used by default;
- You may no longer need to write the implementation if the default one suits your needs.
```ts
import { Integration } from "express-zod-api";
new Integration({ clientClassName: "FancyClient" });
```
```ts
import { FancyClient } from "./generated-client.ts";
const client = new FancyClient(/* optional implementation */);
```
### v22.0.0
- Minimum supported Node versions: 20.9.0 and 22.0.0:
- Node 18 is no longer supported; its end of life is April 30, 2025.
- `BuiltinLogger::profile()` behavior changed for picoseconds: expressing them through nanoseconds;
- Feature: handling all (not just `x-` prefixed) headers as an input source (when enabled):
- Behavior changed for `headers` inside `inputSources` config option: all headers are addressed to the `input` object;
- This change is motivated by the deprecation of `x-` prefixed headers;
- Since the order inside `inputSources` matters, consider moving `headers` to the first place to avoid overwrites;
- The generated `Documentation` recognizes both `x-` prefixed inputs and well-known headers listed on IANA.ORG;
- You can customize that behavior by using the new option `isHeader` of the `Documentation::constructor()`.
- The `splitResponse` property on the `Integration::constructor()` argument is removed;
- Changes to the client code generated by `Integration`:
- The class name changed from `ExpressZodAPIClient` to just `Client`;
- The overload of the `Client::provide()` having 3 arguments and the `Provider` type are removed;
- The public `jsonEndpoints` const is removed — use the `content-type` header of an actual response instead;
- The public type `MethodPath` is removed — use the `Request` type instead.
- The approach to tagging endpoints changed:
- The `tags` property moved from the argument of `createConfig()` to `Documentation::constructor()`;
- The overload of `EndpointsFactory::constructor()` accepting `config` property is removed;
- The argument of `EventStreamFactory::constructor()` is now the events map (formerly assigned to `events` property);
- Tags should be declared as the keys of the augmented interface `TagOverrides` instead;
- The public method `Endpoint::getSecurity()` now returns an array;
- Consider the automated migration using the built-in ESLint rule.
```js
// eslint.config.mjs — minimal ESLint 9 config to apply migrations automatically using "eslint --fix"
import parser from "@typescript-eslint/parser";
import migration from "express-zod-api/migration";
export default [
{ languageOptions: { parser }, plugins: { migration } },
{ files: ["**/*.ts"], rules: { "migration/v22": "error" } },
];
```
```diff
createConfig({
- tags: {},
inputSources: {
- get: ["query", "headers"] // if you have headers on last place
+ get: ["headers", "query"] // move headers to avoid overwrites
}
});
new Documentation({
+ tags: {},
+ isHeader: (name, method, path) => {} // optional
});
new EndpointsFactory(
- { config, resultHandler: new ResultHandler() }
+ new ResultHandler()
);
new EventStreamFactory(
- { config, events: {} }
+ {} // events map only
);
```
```ts
// new tagging approach
import { defaultEndpointsFactory, Documentation } from "express-zod-api";
// Add similar declaration once, somewhere in your code, preferably near config
declare module "express-zod-api" {
interface TagOverrides {
users: unknown;
files: unknown;
subscriptions: unknown;
}
}
// Add extended description of the tags to Documentation (optional)
new Documentation({
tags: {
users: "All about users",
files: { description: "All about files", url: "https://example.com" },
},
});
```
## Version 21
### v21.11.1
- Common styling methods (coloring) are extracted from the built-in logger instance:
- This measure is to reduce memory consumption when using a child logger.
### v21.11.0
- New public property `ctx` is available on instances of `BuiltinLogger`:
- When using the built-in logger and `childLoggerProvider` config option, the `ctx` property contains the argument
that was used for creating the child logger using its `.child()` method;
- It can be utilized for accessing its `requestId` property for purposes other than logging;
- The default value of `ctx` is an empty object (when the instance is not a child logger).
```ts
import { BuiltinLogger, createConfig, createMiddleware } from "express-zod-api";
// Declaring the logger type in use
declare module "express-zod-api" {
interface LoggerOverrides extends BuiltinLogger {}
}
// Configuring child logger provider
const config = createConfig({
childLoggerProvider: ({ parent }) =>
parent.child({ requestId: randomUUID() }),
});
// Accessing child logger context
createMiddleware({
handler: async ({ logger }) => {
doSomething(logger.ctx.requestId); // <—
},
});
```
### v21.10.0
- New `Integration` option: `serverUrl`, string, optional, the API URL for the generated client:
- Currently used for generating example implementation;
- Default value remains `https://example.com`;
- Using `new URL()` for constructing the final request URL in the example implementation of the generated client:
- That enables handling `serverUrl` both with and without trailing slash;
### v21.9.0
- Deprecating `MethodPath` type in the code generated by `Integration`:
- Introducing the `Request` type to be used instead;
- Added JSDoc having the request in description for every type and interface generated by `Integration`;
- A couple adjustments for consistency and performance.
### v21.8.0
- Deprecating `jsonEndpoints` from the code generated by `Integration`:
- Use the `content-type` header of an actual response instead, [example](#v20212).
### v21.7.0
- Feature: introducing `EncodedResponse` public interface in the code generated by `Integration`:
- The new entity should enable making custom clients having response type awereness based on the status code;
- The difference between `Response` and `EndcodedResponse` is the second hierarchical level.
```ts
import { EncodedResponse } from "./generated.ts";
type UsageExample = EncodedResponse["get /v1/user/retrieve"][200];
```
### v21.6.1
- `node-mocks-http` version is `^1.16.2`;
- Fixed possible duplicates in the `Path` type generated by `Integration`:
- Duplicates used to be possible when using `DependsOnMethod` instance within specified `routing`.
### v21.6.0
- Supporting the following `z.string()` formats by the `Documentation` generator:
- `base64` (as `byte`), `date`, `time`, `duration`, `nanoid`;
- And new formats introduced by Zod 3.24: `jwt`, `base64url`, `cidr`;
- Fixed missing `minLength` and `maxLength` properties when depicting `z.string().length()` (fixed length strings).
### v21.5.0
- Feat: Introducing [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events):
- Basic implementation of the event streams feature is now available using `EventStreamFactory` class;
- The new factory is similar to `EndpointsFactory` including the middlewares support;
- Client application can subscribe to the event stream using `EventSource` class instance;
- `Documentation` and `Integration` do not have yet a special depiction of such endpoints;
- This feature is a lightweight alternative to [Zod Sockets](https://github.com/RobinTail/zod-sockets).
```ts
import { z } from "zod";
import { EventStreamFactory } from "express-zod-api";
import { setTimeout } from "node:timers/promises";
const subscriptionEndpoint = EventStreamFactory({
events: { time: z.number().int().positive() },
}).buildVoid({
input: z.object({}), // optional input schema
handler: async ({ options: { emit, isClosed } }) => {
while (!isClosed()) {
emit("time", Date.now());
await setTimeout(1000);
}
},
});
```
```js
const source = new EventSource("https://example.com/api/v1/time");
source.addEventListener("time", (event) => {
const data = JSON.parse(event.data); // number
});
```
### v21.4.0
- Return type of public methods `getTags()` and `getScopes()` of `Endpoint` corrected to `ReadyonlyArray<string>`;
- Featuring `EndpointsFactory::buildVoid()` method:
- It's a shorthand for returning `{}` while having `output` schema `z.object({})`;
- When using this method, `handler` may return `void` while retaining the object-based operation internally.
```diff
- factory.build({
+ factory.buildVoid({
- output: z.object({}),
handler: async () => {
- return {};
},
});
```
### v21.3.0
- Fixed `provide()` method usage example in the code of the generated client;
- Always splitting the response in the generated client:
- This will print the positive and negative response types separately;
- The `splitResponse` property on the `Integration` class constructor argument is deprecated.
### v21.2.0
- Minor performance adjustments;
- Introducing stricter overload for the generated `ExpressZodAPIClient::provide()` method:
- The method can now also accept two arguments: space-separated method with path and parameters;
- Using this overload provides strict constraints on the first argument so that undeclared routes can not be used;
- This design is inspired by the OctoKit and aims to prevent the misuse by throwing a Typescript error.
- Using `ExpressZodAPIClient::provide()` with three arguments is deprecated:
- The return type when using undeclared routes corrected to `unknown`.
- The `Provider` type of the generated client is deprecated;
- The type of the following generated client entities is corrected so that it became limited to the listed routes:
- `Input`, `Response`, `PositiveResponse`, `NegativeResponse`, `MethodPath`.
```diff
- client.provide("get", "/v1/user/retrieve", { id: "10" }); // deprecated
+ client.provide("get /v1/user/retrieve", { id: "10" }); // featured
```
### 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);
- Breaking changes to `createConfig()` argument:
- The `server` property renamed to `http` and made optional — (can now configure HTTPS only);
- These properties moved to the top level: `jsonParser`, `upload`, `compression`, `rawParser` and `beforeRouting`;
- Both `logger` and `getChildLogger` arguments of `beforeRouting` function are replaced with all-purpose `getLogger`.
- Breaking changes to `createServer()` resolved return:
- Both `httpServer` and `httpsServer` are combined into single `servers` property (array, same order).
- Breaking changes to `EndpointsFactory::build()` argument:
- Plural `methods`, `tags` and `scopes` properties replaced with singular `method`, `tag`, `scope` accordingly;
- The `method` property also made optional and can now be derived from `DependsOnMethod` or imply `GET` by default;
- When `method` is assigned with an array, it must be non-empty.
- Breaking changes to `positive` and `negative` properties of `ResultHandler` constructor argument:
- Plural `statusCodes` and `mimeTypes` props within the values are replaced with singular `statusCode` and `mimeType`.
- Other breaking changes:
- The `serializer` property of `Documentation` and `Integration` constructor argument removed;
- The `originalError` property of `InputValidationError` and `OutputValidationError` removed (use `cause` instead);
- The `getStatusCodeFromError()` method removed (use the `ensureHttpError().statusCode` instead);
- The `testEndpoint()` method can no longer test CORS headers — that function moved to `Routing` traverse;
- For `Endpoint`: `getMethods()` may return `undefined`, `getMimeTypes()` removed, `getSchema()` variants reduced;
- Public properties `pairs`, `firstEndpoint` and `siblingMethods` of `DependsOnMethod` replaced with `entries`.
- Consider the automated migration using the built-in ESLint rule.
```js
// eslint.config.mjs — minimal ESLint 9 config to apply migrations automatically using "eslint --fix"
import parser from "@typescript-eslint/parser";
import migration from "express-zod-api/migration";
export default [
{ languageOptions: { parser }, plugins: { migration } },
{ files: ["**/*.ts"], rules: { "migration/v21": "error" } },
];
```
```ts
// The sample of new structure
const config = createConfig({
http: { listen: 80 }, // became optional
https: { listen: 443, options: {} },
upload: true,
compression: true,
beforeRouting: ({ app, getLogger }) => {
const logger = getLogger();
app.use((req, res, next) => {
const childLogger = getLogger(req);
});
},
});
const { servers } = await createServer(config, {});
```
## Version 20
### v20.22.1
- Avoids startup logo distortion when the terminal is too narrow;
- Self-diagnosis for potential problems disabled in production mode to ensure faster startup:
- Warning about potentially unserializable schema for JSON operating endpoints was introduced in v20.15.0.
### v20.22.0
- Featuring a helper to describe nested Routing for already assigned routes:
- Suppose you want to describe `Routing` for both `/v1/path` and `/v1/path/subpath` routes having Endpoints attached;
- Previously, an empty path segment was proposed for that purpose, but there is more elegant and readable way now;
- The `.nest()` method is available both on `Endpoint` and `DependsOnMethod` instances:
```ts
import { Routing } from "express-zod-api";
// Describing routes /v1/path and /v1/path/subpath both having endpoints assigned:
const before: Routing = {
v1: {
path: {
"": endpointA,
subpath: endpointB,
},
},
};
const after: Routing = {
v1: {
path: endpointA.nest({
subpath: endpointB,
}),
},
};
```
### v20.21.2
- Fixed the example implementation in the generated client for endpoints using path params:
- The choice of parser was made based on the exported `const jsonEndpoints` indexed by `path`;
- The actual `path` used for the lookup already contained parameter substitutions so that JSON parser didn't work;
- The new example implementation suggests choosing the parser based on the actual `response.headers`;
- The issue was found and reported by [@HenriJ](https://github.com/HenriJ).
```diff
- const parser = `${method} ${path}` in jsonEndpoints ? "json" : "text";
+ const isJSON = response.headers
+ .get("content-type")
+ ?.startsWith("application/json");
- return response[parser]();
+ return response[isJSON ? "json" : "text"]();
```
### v20.21.1
- Performance tuning: `Routing` traverse made about 12 times faster.
### v20.21.0
- Feat: input schema made optional:
- The `input` property can be now omitted on the argument of the following methods:
`Middlware::constructor`, `EndpointsFactory::build()`, `EndpointsFactory::addMiddleware()`;
- When the input schema is not specified `z.object({})` is used;
- This feature aims to simplify the implementation for Endpoints and Middlwares having no inputs.
### v20.20.1
- Minor code style refactoring and performance tuning;
- The software is redefined as a framework;
- Thanks to [@JonParton](https://github.com/JonParton) for contribution to the documentation.
### v20.20.0
- Introducing `errorHandler` option for `testMiddleware()` method:
- If your middleware throws an error there was no ability to make assertions other than the thrown error;
- New option can be assigned with a function for transforming the error into response, so that `testMiddlware` itself
would not throw, enabling usage of all returned entities for multiple assertions in test;
- The feature suggested by [@williamgcampbell](https://github.com/williamgcampbell).
```ts
import { testMiddleware, Middleware } from "express-zod-api";
const middlware = new Middleware({
input: z.object({}),
handler: async ({ logger }) => {
logger.info("logging something");
throw new Error("something went wrong");
},
});
test("a middleware throws, but it writes log as well", async () => {
const { loggerMock, responseMock } = await testMiddleware({
errorHandler: (error, response) => response.end(error.message),
middleware,
});
expect(loggerMock._getLogs().info).toEqual([["logging something"]]);
expect(responseMock._getData()).toBe("something went wrong");
});
```
### v20.19.0
- Configuring built-in logger made optional:
- Built-in logger configuration option `level` made optional as well as the `logger` option for `createConfig()`;
- Using `debug` level by default, or `warn` when `NODE_ENV=production`.
- Fixed performance issue on `BuiltinLogger` when its `color` option is not set in config:
- `.child()` method is 50x times faster now by only detecting the color support once;
- Color autodetection was introduced in v18.3.0.
### v20.18.0
- Introducing `ensureHttpError()` method that converts any `Error` into `HttpError`:
- It converts `InputValidationError` to `BadRequest` (status code `400`) and others to `InternalServerError` (`500`).
- Deprecating `getStatusCodeFromError()` — use the `ensureHttpError().statusCode` instead.
- Generalizing server-side error messages in production mode by default:
- This feature aims to improve the security of your API by not disclosing the exact causes of errors;
- Applies to `defaultResultHandler`, `defaultEndpointsFactory` and Last Resort Handler only;
- When `NODE_ENV` is set to `production` (displayed on startup);
- Instead of actual message the default one associated with the corresponding `statusCode` used;
- Server-side errors are those having status code `5XX`, or treated that way by `ensureHttpError()`;
- You can control that behavior by throwing errors using `createHttpError()` and using its `expose` option;
- More about production mode and how to activate it:
https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production
```ts
import createHttpError from "http-errors";
// NODE_ENV=production
// Throwing HttpError from Endpoint or Middleware that is using defaultResultHandler or defaultEndpointsFactory:
createHttpError(401, "Token expired"); // —> "Token expired"
createHttpError(401, "Token expired", { expose: false }); // —> "Unauthorized"
createHttpError(500, "Something is broken"); // —> "Internal Server Error"
createHttpError(501, "We didn't make it yet", { expose: true }); // —> "We didn't make it yet"
```
### v20.17.0
- Added `cause` property to `DocumentationError`;
- Log all server side errors (status codes `>= 500`) and in full (not just the `message`).
### v20.16.0
- Deprecating `originalError` property on both `InputValidationError` and `OutputValidationError`:
- Use `cause` property instead;
- Those error classes are publicly exposed for developers making custom Result Handlers.
```diff
const error = new InputValidationError(new z.ZodError([]));
- logger.error(error.originalError.message);
+ logger.error(error.cause.message);
```
### v20.15.3
- Merge intersected object types in generated client:
- This fixes "empty object" intersection problem for endpoints having middlwares without inputs.
```diff
- type GetV1UserRetrieveInput = {} & {
+ type GetV1UserRetrieveInput = {
/** a numeric string containing the id of the user */
id: string;
};
```
### v20.15.2
- Fixed duplicated client types in unions:
- When `splitResponse` option is disabled on `Integration` primitive response types could have been duplicated.
```diff
- type GetV1AvatarSendResponse = string | string;
+ type GetV1AvatarSendResponse = string;
```
### v20.15.1
- Deprecating `serializer` property on `Documentation` and `Integration` constructor argument:
- That property was introduced in v9.3.0 and utilized for comparing schemas in order to handle possible circular
references within `z.lazy()`;
- The property is no longer in use and will be removed in version 21.
### v20.15.0
- Feat: warn about potentially unserializable schema used for JSON operating endpoints:
- This version will warn you if you're using a schema that might not work in request or response, in particular:
- Generally unserializable objects: `z.map()`, `z.set()`, `z.bigint()`;
- JSON incompatible entities: `z.never()`, `z.void()`, `z.promise()`, `z.symbol()`, `z.nan()`;
- Non-revivable in request: `z.date()`;
- Incorrectly used in request: `ez.dateOut()`;
- Incorrectly used in response: `ez.dateIn()`, `ez.upload()`, `ez.raw()`;
- The feature suggested by [@t1nky](https://github.com/t1nky).
### v20.14.3
- Fixed: missing export of `testMiddleware`:
- The feature introduced in v20.4.0 but was not available;
- The issue reported by [@Tomtec331](https://github.com/Tomtec331).
### v20.14.2
- Documentation: promoting Express 5 as the recommended version for new projects;
- Minor refactoring: response variant constraints, inverted definition of `AbstractLogger` type;
- There is now an opportunity to support the project with sponsorship: https://github.com/sponsors/RobinTail
### v20.14.1
- `node-mocks-http` version is `^1.16.1`:
- This deduplicates the `@types/express` dependency to the version installed in your project;
- This fix is an addition to the support of Express 5 (v20.10.0) and its types (v20.14.0).
### v20.14.0
- Enabling usage of recently released `@types/express@^5.0.0`:
- This is an addition to the support of Express 5 introduced in v20.10.0.
### v20.12.0
- Feat: Graceful Shutdown
- You can enable and configure a special request monitoring that,
if it receives a signal to terminate a process, will:
- first put the server into a mode that rejects new requests,
- attempt to complete started requests within the specified time,
- and then forcefully stop the server and terminate the process;
- This feature utilizes a modernized fork of [http-terminator](https://github.com/gajus/http-terminator).
```ts
import { createConfig } from "express-zod-api";
createConfig({
gracefulShutdown: {
timeout: 1000,
events: ["SIGINT", "SIGTERM"],
},
});
```
### v20.11.0
- Feat: Handling deprecation events by actual logger
- `express` uses `depd` for emitting `deprecation` events when a certain deprecated approach used;
- This version installs a listener of that event and delegates it to the `warn` method of your actual logger.
### v20.10.0
- Feat: Supporting Express 5
- Epic news: after 10 years of struggles and anticipations
[Express 5.0.0 is finally released](https://github.com/expressjs/express/releases/tag/v5.0.0);
- The primary a mostly awaited feature is the proper support of asynchronous handlers;
- This version introduces the initial support of Express 5 without breaking changes;
- Notice: the corresponding `@types/express` for the version 5 is not yet released;
- [Instructions on migrating to Express 5](https://expressjs.com/en/guide/migrating-5.html).
### v20.9.2
- Minor syntax adjustments and cleanup;
- `node-mocks-http` version is 1.16.0.
### v20.9.1
- Plain text MIME type is set for the corresponding responses:
- Last Resort Handler (in case your ResultHandler throws);
- ~~`arrayResultHandler`~~ (deprecated) — in case of errors and failures.
### v20.9.0
- `openapi3-ts` version is 4.4.0:
- Feat: `Documentation::getSpecAsYaml()` accepts the same options as `yaml.stringify`.
### v20.8.0
- Feat: providing child logger to `beforeRouting()` hook:
- The function assigned to config property `server.beforeRouting` now accepts additional argument `getChildLogger()`;
- The featured method accepts `request` and returns a child logger if `childLoggerProvider()` is configured;
- Otherwise, it returns the root logger (same for all requests, same as the `logger` argument);
- The feature suggested by [@williamgcampbell](https://github.com/williamgcampbell).
```ts
import { createConfig } from "express-zod-api";
import { randomUUID } from "node:crypto";
const config = createConfig({
logger: { level: "debug" },
childLoggerProvider: ({ parent }) =>
parent.child({ requestId: randomUUID() }),
server: {
listen: 80,
beforeRouting: ({ app, logger, getChildLogger }) => {
logger.info("This is root logger");
app.use((req, res, next) => {
getChildLogger(req).info("This is a child logger");
next();
});
},
},
});
```
### v20.7.1
- Improved documentation on error handling:
- More clarity on the origins of possible runtime errors and how they are handled by default;
- Revealing details on how routing, parsing and upload errors are handled by default;
- Correction to the JSDoc of the corresponding `errorHandler` property in config.
- Removing redundant type coercion in the migration tool.
### v20.7.0
- Changes to migration plugin (single-use tool, regardless SemVer):
- Requirements: `eslint@^9` and `typescript-eslint@^8` (may work with previous versions, but it's no longer tested);
- The `express-zod-api/migration` is a pure ESLint plugin: no rule applied by default, it must be enabled explicitly;
- The files requiring migration have to be defined explicitly — this should improve clarity on its operation;
- The ESLint plugin was introduced in v20.0.0 for automated migration from v19 (except assertions in tests);
- For migrating from v19 use the following minimal config and run `eslint --fix`:
```javascript
// eslint.config.js (or .mjs if you're developing in a CommonJS environment)
import parser from "@typescript-eslint/parser";
import migration from "express-zod-api/migration";
export default [
{ languageOptions: { parser }, plugins: { migration } },
{
files: ["**/*.ts"], // define the files need to be migrated (source code)
rules: { "migration/v20": "error" }, // enable the rule explicitly
},
];
```
### v20.6.2
- Small refactoring of several methods and expressions.
### v20.6.1
- `node-mocks-http` version `^1.15.1`.
### v20.6.0
- Small performance tuning;
- Featuring customizations for profiler of the built-in logger:
- The `.profile()` method can now accept an object having the following properties:
- `message` — the one to be displayed;
- `formatter` — optional, a function to transform milliseconds into a string or number;
- `severity` — optional, `debug` (default), `info`, `warn`, `error`:
- it can also be a function returning one of those values depending on duration in milliseconds;
- thus, you can immediately assess the measured performance.
```typescript
const done = logger.profile({
message: "expensive operation",
severity: (ms) => (ms > 500 ? "error" : "info"),
formatter: (ms) => `${ms.toFixed(2)}ms`,
});
doExpensiveOperation();
done(); // error: expensive operation '555.55ms'
```
### v20.5.0
- Featuring a simple profiler for the built-in logger:
- Introducing `BuiltinLogger::profile(msg: string)` — measures the duration until you invoke the returned callback;
- Using Node Performance Hooks for measuring microtimes (less than 1ms);
- The output severity is `debug` (will be customizable later), so logger must have the corresponding `level`;
- It prints the duration in log using adaptive units: from picoseconds to minutes.
```typescript
// usage assuming that logger is an instance of BuiltinLogger
const done = logger.profile("expensive operation");
doExpensiveOperation();
done(); // debug: expensive operation '555 milliseconds'
```
```typescript
// to set up config using the built-in logger do this:
import { createConfig, BuiltinLogger } from "express-zod-api";
const config = createConfig({ logger: { level: "debug", color: true } });
declare module "express-zod-api" {
interface LoggerOverrides extends BuiltinLogger {}
}
```
### v20.4.1
- Technical update due to improved builder configuration:
- Removed crutches for the `migration/index.d.cts` file;
- Fixed missing `node:` protocol in the imports of core modules in the distributed javascript files.
### v20.4.0
- Feat: middleware testing helper: `testMiddleware()`, similar to `testEndpoint()`:
- There is also an ability to pass `options` collected from outputs of previous middlewares, if the one being tested
somehow depends on them.
- The method returns: `Promise<{ output, requestMock, responseMock, loggerMock }>`;
- Export fixed in v20.14.3.
```typescript
import { z } from "zod";
import { Middleware, testMiddleware } from "express-zod-api";
const middleware = new Middleware({
input: z.object({ test: z.string() }),
handler: async ({ options, input: { test } }) => ({
collectedOptions: Object.keys(options),
testLength: test.length,
}),
});
const { output, responseMock, loggerMock } = await testMiddleware({
middleware,
requestProps: { method: "POST", body: { test: "something" } },
options: { prev: "accumulated" }, // responseOptions, configProps, loggerProps
});
expect(loggerMock._getLogs().error).toHaveLength(0);
expect(output).toEqual({ collectedOptions: ["prev"], testLength: 9 });
```
### v20.3.2
- Minor corrections to the documentation.
### v20.3.1
- Removed `eslint` and `prettier` from the list of the optional peer dependencies:
- `eslint` with a flat config support (v8 or v9) is only required to use [the migration codemod](#v2000);
- `prettier` is only a fallback for `Integration::printFormatted()`, which can work without it as well;
- These changes aim to reduce the confusion and ease the installation;
- The issue was found and reported by **Bogdan** who does not have a GitHub account.
### v20.3.0
- Feature: `z.object().remap()` accepts a mapping function:
- Similar to `.transform()` you can now supply an object shape mapping function;
- It is important to use shallow transformations only;
- Using `.remap()` is recommended for `output` schemas if you're also aiming to generate a valid documentation.
```ts
import camelize from "camelize-ts";
import snakify from "snakify-ts";
import { z } from "zod";
const endpoint = endpointsFactory.build({
method: "get",
input: z
.object({ user_id: z.string() })
.transform((inputs) => camelize(inputs, /* shallow: */ true)),
output: z
.object({ userName: z.string() })
.remap((outputs) => snakify(outputs, /* shallow: */ true)),
handler: async ({ input: { userId }, logger }) => {
logger.debug("user_id became userId", userId);
return { userName: "Agneta" }; // becomes "user_name" in response
},
});
```
### v20.2.0
- Feature: Partial mapping and passthrough support for `z.object().remap()`:
- Properties can be omitted in `remap()` in order to preserve them unchanged;
- Undeclared keys will remain unchanged for `z.object().passthrough().remap()` schema;
- Passthrough object schemas are not allowed in Middlewares, but they are allowed in Endpoints.
```ts
z.object({ user_name: z.string(), id: z.number() }).remap({
user_name: "userName", // —> { userName, id }
});
z.object({ user_id: z.string() })
.passthrough()
.remap({ user_id: "userId" })
.parse({ user_id: "test", extra: "excessive" }); // —> { userId, extra }
```
### v20.1.0
- Feature: Top level transformations support and object schema remapping:
- This can enable having `snake_case` API parameters while keeping `camelCase` naming in your implementation;
- You can `.transform()` the entire `input` schema into another object, using a well-typed mapping library;
- You can do the same with the `output` schema, but that would not be enough for generating a valid documentation;
- The framework offers a new `.remap()` method on the `z.object()` schema that applies a `.pipe()` to transformation;
- Currently `.remap()` requires an assignment of all the object props explicitly, but it may be improved later;
- Find more details [in the documentation](README.md#top-level-transformations-and-mapping);
- The feature suggested by [Peter Rottmann](https://github.com/rottmann).
```ts
import camelize from "camelize-ts";
import { z } from "zod";
const endpoint = endpointsFactory.build({
method: "get",
input: z
.object({ user_id: z.string() })
.transform((inputs) => camelize(inputs, true)), // shallow
output: z.object({ userName: z.string() }).remap({ userName: "user_name" }),
handler: async ({ input: { userId }, logger }) => {
logger.debug("user_id became userId", userId);
return { userName: "Agneta" }; // becomes "user_name" in response
},
});
```
### v20.0.1
- Found a better method for asserting the expected response: `responseMock._getJSONData()`.
### v20.0.0
- Method `createLogger()` removed — use `new BuiltinLogger()` instead if needed;
- Method `createResultHandler` removed — use `new ResultHandler()` instead:
- The argument's properties renamed: `getPositiveResponse` to `positive` and `getNegativeResponse` to `negative`;
- Both properties can now accept static values (not only functions).
- Method `createMiddleware()` removed — use either `new Middleware()` or `EndpointsFactory::addMiddleware()` instead:
- The argument's property `middleware` renamed to `handler`.
- Method `testEndpoint()` was changed:
- It was detached from any testing frameworks, `fnMethod` property removed from the argument;
- Mocked request and response are now fully operational and do not require to mock anything to do the job;
- The `responseProps` property changed to `responseOptions`, it's no longer meant to be used for custom props;
- The returned entities `requestMock`, `responseMock` and `loggerMock` no longer rely on testing framework for props.
Instead, they provide methods to assert expectations in tests:
- `responseMock._getStatusCode()`, `responseMock._getHeaders()`, `responseMock._getData()`, `loggerMock._getLogs()`;
- See [the documentation of node-mocks-http library](https://www.npmjs.com/package/node-mocks-http) for details.
- How to migrate:
- Consider using the provided ESLint plugin `migration` in order to apply changes automatically (except assertions);
- Or follow the code samples below in order to rename/remove entities manually as described above.
```js
// eslint.config.mjs — minimal config to apply migrations automatically using "eslint . --fix" (at least ESLint 8)
import parser from "@typescript-eslint/parser";
import migration from "express-zod-api/migration";
export default [{ languageOptions: { parser }, files: ["**/*.ts"] }, migration];
```
```ts
// before
createResultHandler({
getPositiveResponse: (data) => z.object({ data }),
getNegativeResponse: () => ({
schema: z.string(),
mimeType: "text/plain",
}),
});
// after
new ResultHandler({
positive: (data) => z.object({ data }),
negative: { schema: z.string(), mimeType: "text/plain" }, // can be static now
});
```
```ts
// before
factory.addMiddleware(
createMiddleware({
input: z.object({}),
middleware: async () => ({}),
}),
);
// after
factory // variant 1:
.addMiddleware(
new Middleware({
input: z.object({}),
handler: async () => ({}),
}),
) // variant 2: short syntax now available:
.addMiddleware({ input: z.object({}), ha