Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
318e980
Adjusting trigger branches.
RobinTail Sep 15, 2025
5a38e17
Resetting the migration.
RobinTail Sep 15, 2025
4e9bd1b
Changelog: v26 draft.
RobinTail Sep 16, 2025
bb5d891
Merge branch 'master' into make-v26
RobinTail Sep 17, 2025
8d4a69a
Removing `DependsOnMethod` (#2938)
RobinTail Sep 17, 2025
da522b9
Merge branch 'master' into make-v26
RobinTail Sep 19, 2025
a8138fb
Merge branch 'master' into make-v26
RobinTail Sep 21, 2025
a695644
Merge branch 'master' into make-v26
RobinTail Sep 26, 2025
d0a135b
Renaming `options` to `ctx` (#2934)
RobinTail Sep 26, 2025
56e01db
Merge branch 'master' into make-v26
RobinTail Sep 26, 2025
6b78479
Merge branch 'master' into make-v26
RobinTail Sep 29, 2025
86bf411
Merge branch 'master' into make-v26
RobinTail Oct 1, 2025
f24859c
Merge branch 'master' into make-v26
RobinTail Oct 2, 2025
08862f0
Merge branch 'master' into make-v26
RobinTail Oct 4, 2025
e6f9e25
merge from master, tsconfig module node20.
RobinTail Oct 4, 2025
741f319
merge from master, dynamic ruleName.
RobinTail Oct 4, 2025
c72ef1f
Updating compatibility test.
RobinTail Oct 4, 2025
661201e
jsdoc: Routing example for removed DependsOnMethod.
RobinTail Oct 4, 2025
d8ccf34
Merge branch 'master' into make-v26
RobinTail Oct 6, 2025
456863e
Updating snapshot.
RobinTail Oct 6, 2025
696a131
Merge branch 'master' into make-v26
RobinTail Oct 9, 2025
a52a7d5
Merge branch 'master' into make-v26
RobinTail Oct 21, 2025
ec2298d
Merge branch 'master' into make-v26
RobinTail Oct 24, 2025
b15c0be
Merge branch 'master' into make-v26
RobinTail Oct 28, 2025
fc3d302
Merge branch 'master' into make-v26
RobinTail Nov 8, 2025
547b13f
Merge branch 'master' into make-v26
RobinTail Nov 8, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ name: "CodeQL"

on:
push:
branches: [ master, v21, v22, v23, v24 ]
branches: [ master, v21, v22, v23, v24, make-v26 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master, v21, v22, v23, v24 ]
branches: [ master, v21, v22, v23, v24, make-v26 ]
schedule:
- cron: '26 8 * * 1'

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ master, v21, v22, v23, v24 ]
branches: [ master, v21, v22, v23, v24, make-v26 ]
pull_request:
branches: [ master, v21, v22, v23, v24 ]
branches: [ master, v21, v22, v23, v24, make-v26 ]

jobs:
build:
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## Version 26

### v26.0.0

- `DependsOnMethod` removed: use flat syntax with explicit method and a slash;
- `options` property renamed to `ctx` in argument of:
- `Middleware::handler()`,
- `ResultHandler::handler()`,
- `handler` of `EndpointsFactory::build()` argument,
- `testMiddleware()`;
- `EndpointsFactory::addOptions()` renamed to `addContext()`;

```patch
const routing: Routing = {
- "/v1/users": new DependsOnMethod({
+ "/v1/users": {
- get: getUserEndpoint,
+ "get /": getUserEndpoint,
- }).nest({
create: makeUserEndpoint
- }),
+ },
};
```

## Version 25

### v25.6.0
Expand Down
71 changes: 35 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Start your API server with I/O schema validation and custom middlewares in minut
4. [Basic features](#basic-features)
1. [Routing](#routing) including static file serving
2. [Middlewares](#middlewares)
3. [Options](#options)
3. [Context](#context)
4. [Using native express middlewares](#using-native-express-middlewares)
5. [Refinements](#refinements)
6. [Query string parser](#query-string-parser)
Expand Down Expand Up @@ -149,7 +149,7 @@ These people contributed to the improvement of the framework by reporting bugs,
The API operates object schemas for input and output validation.
The object being validated is the combination of certain `request` properties.
It is available to the endpoint handler as the `input` parameter.
Middlewares have access to all `request` properties, they can provide endpoints with `options`.
Middlewares have access to all `request` properties, they can provide endpoints with `ctx` (context).
The object returned by the endpoint handler is called `output`. It goes to the `ResultHandler` which is
responsible for transmitting consistent responses containing the `output` or possible error.
Much can be customized to fit your needs.
Expand Down Expand Up @@ -233,8 +233,8 @@ const helloWorldEndpoint = defaultEndpointsFactory.build({
output: z.object({
greetings: z.string(),
}),
handler: async ({ input: { name }, options, logger }) => {
logger.debug("Options:", options); // middlewares provide options
handler: async ({ input: { name }, ctx, logger }) => {
logger.debug("Context:", ctx); // middlewares provide ctx
return { greetings: `Hello, ${name || "World"}. Happy coding!` };
},
});
Expand Down Expand Up @@ -288,7 +288,7 @@ in one place, illustrating how you can structure your API using whichever method
architecture — or even mix them seamlessly.

```ts
import { Routing, DependsOnMethod, ServeStatic } from "express-zod-api";
import { Routing, ServeStatic } from "express-zod-api";

const routing: Routing = {
// flat syntax — /v1/users
Expand All @@ -306,12 +306,12 @@ const routing: Routing = {
// mixed syntax with explicit method — /v1/user/:id
"delete /user/:id": deleteUserEndpoint,
// method-based routing — /v1/account
account: new DependsOnMethod({
get: endpointA,
delete: endpointA,
post: endpointB,
patch: endpointB,
}),
account: {
"get /": endpointA,
"delete /": endpointA,
"post /": endpointB,
"patch /": endpointB,
},
},
// static file serving — /public serves files from ./assets
public: new ServeStatic("assets", {
Expand All @@ -329,7 +329,7 @@ When the method is not specified, the one(s) supported by the Endpoint applied (

## Middlewares

Middleware can authenticate using input or `request` headers, and can provide endpoint handlers with `options`.
Middleware can authenticate using input or `request` headers, and can provide endpoint handlers with `ctx`.
Inputs of middlewares are also available to endpoint handlers within `input`.

Here is an example of the authentication middleware, that checks a `key` from input and `token` from headers:
Expand All @@ -356,7 +356,7 @@ const authMiddleware = new Middleware({
if (!user) throw createHttpError(401, "Invalid key");
if (request.headers.token !== user.token)
throw createHttpError(401, "Invalid token");
return { user }; // provides endpoints with options.user
return { user }; // provides endpoints with ctx.user
},
});
```
Expand All @@ -367,36 +367,36 @@ By using `.addMiddleware()` method before `.build()` you can connect it to the e
const yourEndpoint = defaultEndpointsFactory
.addMiddleware(authMiddleware)
.build({
handler: async ({ options: { user } }) => {
handler: async ({ ctx: { user } }) => {
// user is the one returned by authMiddleware
}, // ...
});
```

You can create a new factory by connecting as many middlewares as you want — they will be executed in the specified
order for all the endpoints produced on that factory. You may also use a shorter inline syntax within the
`.addMiddleware()` method, and have access to the output of the previously executed middlewares in chain as `options`:
`.addMiddleware()` method, and have access to the output of the previously executed middlewares in chain as `ctx`:

```ts
import { defaultEndpointsFactory } from "express-zod-api";

const factory = defaultEndpointsFactory
.addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:
.addMiddleware({
handler: async ({ options: { user } }) => ({}), // user from authMiddleware
handler: async ({ ctx: { user } }) => ({}), // user from authMiddleware
});
```

## Options
## Context

In case you'd like to provide your endpoints with options that do not depend on Request, like non-persistent connection
to a database, consider shorthand method `addOptions`. For static options consider reusing `const` across your files.
If you need to provide your endpoints with a context that does not depend on Request, like non-persistent database
connection, consider shorthand method `addContext`. For static values consider reusing a `const` across your files.

```ts
import { readFile } from "node:fs/promises";
import { defaultEndpointsFactory } from "express-zod-api";

const endpointsFactory = defaultEndpointsFactory.addOptions(async () => {
const endpointsFactory = defaultEndpointsFactory.addContext(async () => {
// caution: new connection on every request:
const db = mongoose.connect("mongodb://connection.string");
const privateKey = await readFile("private-key.pem", "utf-8");
Expand All @@ -411,10 +411,10 @@ custom [Result Handler](#response-customization):
import { ResultHandler } from "express-zod-api";

const resultHandlerWithCleanup = new ResultHandler({
handler: ({ options }) => {
// necessary to check for certain option presence:
if ("db" in options && options.db) {
options.db.connection.close(); // sample cleanup
handler: ({ ctx }) => {
// necessary to check the presence of a certain property:
if ("db" in ctx && ctx.db) {
ctx.db.connection.close(); // sample cleanup
}
},
});
Expand Down Expand Up @@ -446,7 +446,7 @@ const config = createConfig({

In case you need a special processing of `request`, or to modify the `response` for selected endpoints, use the method
`addExpressMiddleware()` of `EndpointsFactory` (or its alias `use()`). The method has two optional features: a provider
of [options](#options) and an error transformer for adjusting the response status code.
of a [context](#context) and an error transformer for adjusting the response status code.

```ts
import { defaultEndpointsFactory } from "express-zod-api";
Expand Down Expand Up @@ -1045,8 +1045,8 @@ test("should respond successfully", async () => {

## Testing middlewares

Middlewares can also be tested individually using the `testMiddleware()` method. You can also pass `options` collected
from outputs of previous middlewares, if the one being tested somehow depends on them. Possible errors would be handled
Middlewares can also be tested individually using the `testMiddleware()` method. You can also pass `ctx` collected
from returns of previous middlewares, if the one being tested somehow depends on it. Possible errors would be handled
either by `errorHandler` configured within given `configProps` or `defaultResultHandler`.

```ts
Expand All @@ -1055,19 +1055,19 @@ 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),
handler: async ({ ctx, input: { test } }) => ({
collectedContext: Object.keys(ctx),
testLength: test.length,
}),
});

const { output, responseMock, loggerMock } = await testMiddleware({
middleware,
requestProps: { method: "POST", body: { test: "something" } },
options: { prev: "accumulated" }, // responseOptions, configProps, loggerProps
ctx: { prev: "accumulated" }, // responseOptions, configProps, loggerProps
});
expect(loggerMock._getLogs().error).toHaveLength(0);
expect(output).toEqual({ collectedOptions: ["prev"], testLength: 9 });
expect(output).toEqual({ collectedContext: ["prev"], testLength: 9 });
```

# Integration and Documentation
Expand Down Expand Up @@ -1183,11 +1183,11 @@ new Documentation({
## Deprecated schemas and routes

As your API evolves, you may need to mark some parameters or routes as deprecated before deleting them. For this
purpose, the `.deprecated()` method is available on each schema, `Endpoint` and `DependsOnMethod`, it's immutable.
purpose, the `.deprecated()` method is available on each schema and `Endpoint`, it's immutable.
You can also deprecate all routes the `Endpoint` assigned to by setting `EndpointsFactory::build({ deprecated: true })`.

```ts
import { Routing, DependsOnMethod } from "express-zod-api";
import { Routing } from "express-zod-api";
import { z } from "zod";

const someEndpoint = factory.build({
Expand All @@ -1199,8 +1199,7 @@ const someEndpoint = factory.build({

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)
v2: someEndpoint, // the path is assigned with initially deprecated endpoint (also deprecated)
};
```

Expand Down Expand Up @@ -1378,7 +1377,7 @@ const subscriptionEndpoint = new EventStreamFactory({
time: z.int().positive(),
}).buildVoid({
input: z.object({}), // optional input schema
handler: async ({ options: { emit, isClosed, signal } }) => {
handler: async ({ ctx: { emit, isClosed, signal } }) => {
while (!isClosed()) {
emit("time", Date.now());
await setTimeout(1000);
Expand Down
2 changes: 1 addition & 1 deletion compat-test/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import migration from "@express-zod-api/migration";

export default [
{ languageOptions: { parser }, plugins: { migration } },
{ files: ["**/*.ts"], rules: { "migration/v25": "error" } },
{ files: ["**/*.ts"], rules: { "migration/v26": "error" } },
];
2 changes: 1 addition & 1 deletion compat-test/migration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { describe, test, expect } from "vitest";
describe("Migration", () => {
test("should fix the import", async () => {
const fixed = await readFile("./sample.ts", "utf-8");
expect(fixed).toBe('import {} from "zod";\n');
expect(fixed).toBe(`const route = {\n"get /": someEndpoint,\n}\n`);
});
});
2 changes: 1 addition & 1 deletion compat-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"private": true,
"scripts": {
"pretest": "echo 'import {} from \"zod/v4\";' > sample.ts",
"pretest": "echo 'const route = new DependsOnMethod({ get: someEndpoint })' > sample.ts",
"test": "eslint --fix && vitest --run",
"posttest": "rm sample.ts"
},
Expand Down
2 changes: 1 addition & 1 deletion dataflow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion example/endpoints/retrieve-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const retrieveUserEndpoint = defaultEndpointsFactory
name: z.string(),
features: feature.array(), // @link https://github.com/colinhacks/zod/issues/4592
}),
handler: async ({ input: { id }, options: { method }, logger }) => {
handler: async ({ input: { id }, ctx: { method }, logger }) => {
logger.debug(`Requested id: ${id}, method ${method}`);
const name = "John Doe";
assert(id <= 100, createHttpError(404, "User not found"));
Expand Down
6 changes: 1 addition & 5 deletions example/endpoints/time-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ export const subscriptionEndpoint = eventsFactory.buildVoid({
.deprecated()
.describe("for testing error response"),
}),
handler: async ({
input: { trigger },
options: { emit, isClosed },
logger,
}) => {
handler: async ({ input: { trigger }, ctx: { emit, isClosed }, logger }) => {
if (trigger === "failure") throw new Error("Intentional failure");
while (!isClosed()) {
logger.debug("emitting");
Expand Down
2 changes: 1 addition & 1 deletion example/endpoints/update-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const updateUserEndpoint =
}),
handler: async ({
input: { id, name },
options: { authorized }, // comes from authMiddleware
ctx: { authorized }, // comes from authMiddleware
logger,
}) => {
logger.debug(`${authorized} is changing user #${id}`);
Expand Down
12 changes: 6 additions & 6 deletions example/routing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DependsOnMethod, Routing, ServeStatic } from "express-zod-api";
import { Routing, ServeStatic } from "express-zod-api";
import { rawAcceptingEndpoint } from "./endpoints/accept-raw.ts";
import { createUserEndpoint } from "./endpoints/create-user.ts";
import { deleteUserEndpoint } from "./endpoints/delete-user.ts";
Expand All @@ -16,12 +16,12 @@ export const routing: Routing = {
user: {
// syntax 1: methods are defined within the endpoint
retrieve: retrieveUserEndpoint, // path: /v1/user/retrieve
// 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({
// id is the route path param
":id": {
remove: deleteUserEndpoint, // nested path: /v1/user/:id/remove
}),
// syntax 2: methods are defined within the route
"patch /": updateUserEndpoint, // demonstrates authentication
},
// demonstrates different response schemas depending on status code
create: createUserEndpoint,
// this one demonstrates the legacy array based response
Expand Down
34 changes: 0 additions & 34 deletions express-zod-api/src/depends-on-method.ts

This file was deleted.

Loading