From fc6ea0f61ce3840ddc0b2dac71a3f8ea01d69d20 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 23 Apr 2026 21:53:31 +0200 Subject: [PATCH 01/16] fix(flattenIO): optimizing performance for walking stack (1.8-19x). --- express-zod-api/bench/can-merge.bench.ts | 64 ---------------------- express-zod-api/bench/queue-ops.bench.ts | 43 +++++++++++++++ express-zod-api/src/json-schema-helpers.ts | 5 +- 3 files changed, 46 insertions(+), 66 deletions(-) delete mode 100644 express-zod-api/bench/can-merge.bench.ts create mode 100644 express-zod-api/bench/queue-ops.bench.ts diff --git a/express-zod-api/bench/can-merge.bench.ts b/express-zod-api/bench/can-merge.bench.ts deleted file mode 100644 index 0a81069b0..000000000 --- a/express-zod-api/bench/can-merge.bench.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { bench, describe } from "vitest"; -import * as R from "ramda"; - -const mergeableKeys = new Set([ - "type", - "properties", - "required", - "examples", - "description", - "additionalProperties", -]); - -const current = (subject: Record) => - R.pipe( - Object.keys, - R.without([ - "type", - "properties", - "required", - "examples", - "description", - "additionalProperties", - ] as string[]), - R.isEmpty, - )(subject); - -const featured = (subject: Record): boolean => { - for (const key of Object.keys(subject)) - if (!mergeableKeys.has(key)) return false; - return true; -}; - -describe.each([ - {}, - { type: "object" }, - { type: "object", properties: {} }, - { type: "object", properties: {}, required: [] }, - { type: "object", properties: {}, required: [], examples: [] }, - { - type: "object", - properties: {}, - required: [], - examples: [], - description: "test", - }, - { - type: "object", - properties: {}, - required: [], - examples: [], - description: "test", - additionalProperties: false, - }, - { type: "object", title: "test" }, - { type: "object", format: "date-time", title: "test" }, -])("Experiment for canMerge %#", (subject) => { - bench(`current`, () => { - current(subject); - }); - - bench(`featured`, () => { - featured(subject); - }); -}); diff --git a/express-zod-api/bench/queue-ops.bench.ts b/express-zod-api/bench/queue-ops.bench.ts new file mode 100644 index 000000000..bc8ef4fec --- /dev/null +++ b/express-zod-api/bench/queue-ops.bench.ts @@ -0,0 +1,43 @@ +import { bench, describe } from "vitest"; + +const smallQueue = Array.from({ length: 5 }, (_, i) => ({ + id: i, + data: `item-${i}`, +})); +const mediumQueue = Array.from({ length: 20 }, (_, i) => ({ + id: i, + data: `item-${i}`, +})); +const largeQueue = Array.from({ length: 100 }, (_, i) => ({ + id: i, + data: `item-${i}`, +})); + +const queues = [ + [smallQueue, "small (5)"], + [mediumQueue, "medium (20)"], + [largeQueue, "large (100)"], +] as const; + +describe.each(queues)("$1", (queue) => { + bench("shift() approach", () => { + const q = [...queue]; + while (q.length) q.shift(); + }); + + bench("index approach", () => { + const q = [...queue]; + let idx = 0; + while (idx < q.length) idx++; + }); + + bench("pop() reverse", () => { + const q = [...queue].reverse(); + while (q.length) q.pop(); + }); + + bench("forEach", () => { + const q = [...queue]; + q.forEach(() => {}); + }); +}); diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index daa103505..5076b11d0 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -107,8 +107,9 @@ export const flattenIO = ( const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] const flat: FlattenObjectSchema = { type: "object", properties: {} }; const flatRequired: string[] = []; - while (stack.length) { - const [isOptional, entry] = stack.shift()!; + let idx = 0; + while (idx < stack.length) { + const [isOptional, entry] = stack[idx++]; if (entry.description) flat.description ??= entry.description; stack.push(...processAllOf(entry, mode, isOptional)); stack.push(...processVariants(entry)); From aab50b1960fe5d8d7861c2fb3c13545a78f6880e Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 23 Apr 2026 22:07:07 +0200 Subject: [PATCH 02/16] fix(hasCycle): optimizing traverse for performance. --- express-zod-api/src/deep-checks.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/deep-checks.ts b/express-zod-api/src/deep-checks.ts index 0b167ad15..08711e9d4 100644 --- a/express-zod-api/src/deep-checks.ts +++ b/express-zod-api/src/deep-checks.ts @@ -39,8 +39,9 @@ export const hasCycle = ( ) => { const json = z.toJSONSchema(subject, { io, unrepresentable: "any" }); const stack: unknown[] = [json]; - while (stack.length) { - const entry = stack.shift()!; + let idx = 0; + while (idx < stack.length) { + const entry = stack[idx++]; if (R.is(Object, entry)) { if ((entry as z.core.JSONSchema.BaseSchema).$ref === "#") return true; stack.push(...R.values(entry)); From 17cede32cdf4b079b94a273342f4dad483ec84b5 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 23 Apr 2026 22:07:52 +0200 Subject: [PATCH 03/16] fix(Diagnostics): optimizing traverse of checkSchema() method for performance. --- express-zod-api/src/diagnostics.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 6e96515b7..91e9c3fd5 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -29,8 +29,9 @@ export class Diagnostics { const stack: z.core.JSONSchema.BaseSchema[] = [ z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), ]; - while (stack.length > 0) { - const entry = stack.shift()!; + let idx = 0; + while (idx < stack.length) { + const entry = stack[idx++]; if (entry.type && entry.type !== "object") this.logger.warn(`Endpoint ${dir} schema is not object-based`, ctx); for (const prop of ["allOf", "oneOf", "anyOf"] as const) From d4f592719850a7cf500da8387d657a2e5cd4d393 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 23 Apr 2026 22:08:57 +0200 Subject: [PATCH 04/16] fix(fixReferences): optimizing traverse for performance. --- express-zod-api/src/documentation-helpers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index c9a646929..8db2674f7 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -371,8 +371,9 @@ const fixReferences = ( ctx: OpenAPIContext, ) => { const stack: unknown[] = [subject, defs]; - while (stack.length) { - const entry = stack.shift()!; + let idx = 0; + while (idx < stack.length) { + const entry = stack[idx++]; if (R.is(Object, entry)) { if (isReferenceObject(entry) && !entry.$ref.startsWith("#/components")) { const actualName = entry.$ref.split("/").pop()!; From c753a10a0e8b4dc84a6b9c739fb8ec7e2ebc6ddf Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 23 Apr 2026 22:31:13 +0200 Subject: [PATCH 05/16] fix(walkRouting): optimizing traverse for performance, O(1) read, still O(n) writing nested. --- express-zod-api/src/routing-walker.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/express-zod-api/src/routing-walker.ts b/express-zod-api/src/routing-walker.ts index 9dba9564e..b7e936ea1 100644 --- a/express-zod-api/src/routing-walker.ts +++ b/express-zod-api/src/routing-walker.ts @@ -91,8 +91,9 @@ export const walkRouting = ({ }: RoutingWalkerParams) => { const stack = processEntries(config, routing); const visited = new Set(); - while (stack.length) { - const [path, element, explicitMethod] = stack.shift()!; + let idx = 0; + while (idx < stack.length) { + const [path, element, explicitMethod] = stack[idx++]; if (element instanceof AbstractEndpoint) { if (explicitMethod) { checkDuplicate(explicitMethod, path, visited); @@ -110,7 +111,7 @@ export const walkRouting = ({ if (element instanceof ServeStatic) { if (onStatic) element.apply(path, onStatic); } else { - stack.unshift(...processEntries(config, element, path)); + stack.splice(idx, 0, ...processEntries(config, element, path)); } } } From 60834a64450e8975f72fc7d3274d1db4f11780ed Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 07:01:00 +0200 Subject: [PATCH 06/16] fix(zts): optimizing onTemplateLiteral for performance. --- express-zod-api/src/zts.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts index db4d7e36a..acbd009e6 100644 --- a/express-zod-api/src/zts.ts +++ b/express-zod-api/src/zts.ts @@ -41,12 +41,13 @@ const onTemplateLiteral: Producer = ( { next, api }, ) => { const parts = [...def.parts]; + let idx = 0; const shiftText = () => { let text = ""; - while (parts.length) { - const part = parts.shift(); + while (idx < parts.length) { + const part = parts[idx++]; if (isSchema(part)) { - parts.unshift(part); + idx--; break; } text += part ?? ""; // Handle potential undefined values @@ -55,12 +56,13 @@ const onTemplateLiteral: Producer = ( }; const head = api.f.createTemplateHead(shiftText()); const spans: ts.TemplateLiteralTypeSpan[] = []; - while (parts.length) { - const schema = next(parts.shift() as z.core.$ZodType); + while (idx < parts.length) { + const schema = next(parts[idx++] as z.core.$ZodType); const text = shiftText(); - const textWrapper = parts.length - ? api.f.createTemplateMiddle - : api.f.createTemplateTail; + const textWrapper = + idx < parts.length + ? api.f.createTemplateMiddle + : api.f.createTemplateTail; spans.push(api.f.createTemplateLiteralTypeSpan(schema, textWrapper(text))); } if (!spans.length) return api.makeLiteralType(head.text); From fb85aa75391bcc68d020211b7ecdd3e78df1ef0c Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 07:18:03 +0200 Subject: [PATCH 07/16] fix(onTemplateLiteral): naming, rm redundant copy. --- express-zod-api/bench/queue-ops.bench.ts | 2 +- express-zod-api/src/zts.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/express-zod-api/bench/queue-ops.bench.ts b/express-zod-api/bench/queue-ops.bench.ts index bc8ef4fec..88297e218 100644 --- a/express-zod-api/bench/queue-ops.bench.ts +++ b/express-zod-api/bench/queue-ops.bench.ts @@ -28,7 +28,7 @@ describe.each(queues)("$1", (queue) => { bench("index approach", () => { const q = [...queue]; let idx = 0; - while (idx < q.length) idx++; + while (idx < q.length) q[idx++]; // eslint-disable-line @typescript-eslint/no-unused-expressions }); bench("pop() reverse", () => { diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts index acbd009e6..ffa505bcc 100644 --- a/express-zod-api/src/zts.ts +++ b/express-zod-api/src/zts.ts @@ -40,9 +40,9 @@ const onTemplateLiteral: Producer = ( { _zod: { def } }: z.core.$ZodTemplateLiteral, { next, api }, ) => { - const parts = [...def.parts]; + const { parts } = def; let idx = 0; - const shiftText = () => { + const readText = () => { let text = ""; while (idx < parts.length) { const part = parts[idx++]; @@ -54,11 +54,11 @@ const onTemplateLiteral: Producer = ( } return text; }; - const head = api.f.createTemplateHead(shiftText()); + const head = api.f.createTemplateHead(readText()); const spans: ts.TemplateLiteralTypeSpan[] = []; while (idx < parts.length) { const schema = next(parts[idx++] as z.core.$ZodType); - const text = shiftText(); + const text = readText(); const textWrapper = idx < parts.length ? api.f.createTemplateMiddle From 4b5e4adce1fd797867001892fe95116ad6a35d36 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 08:03:06 +0200 Subject: [PATCH 08/16] FIX: replacing let+while with simpler for loop everywhere except zts module yet. --- express-zod-api/bench/queue-ops.bench.ts | 3 +-- express-zod-api/src/deep-checks.ts | 5 ++--- express-zod-api/src/diagnostics.ts | 5 ++--- express-zod-api/src/documentation-helpers.ts | 5 ++--- express-zod-api/src/json-schema-helpers.ts | 5 ++--- express-zod-api/src/routing-walker.ts | 7 +++---- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/express-zod-api/bench/queue-ops.bench.ts b/express-zod-api/bench/queue-ops.bench.ts index 88297e218..80bf84c46 100644 --- a/express-zod-api/bench/queue-ops.bench.ts +++ b/express-zod-api/bench/queue-ops.bench.ts @@ -27,8 +27,7 @@ describe.each(queues)("$1", (queue) => { bench("index approach", () => { const q = [...queue]; - let idx = 0; - while (idx < q.length) q[idx++]; // eslint-disable-line @typescript-eslint/no-unused-expressions + for (let idx = 0; idx < q.length; idx++) q[idx]; // eslint-disable-line @typescript-eslint/no-unused-expressions }); bench("pop() reverse", () => { diff --git a/express-zod-api/src/deep-checks.ts b/express-zod-api/src/deep-checks.ts index 08711e9d4..7a4eb2586 100644 --- a/express-zod-api/src/deep-checks.ts +++ b/express-zod-api/src/deep-checks.ts @@ -39,9 +39,8 @@ export const hasCycle = ( ) => { const json = z.toJSONSchema(subject, { io, unrepresentable: "any" }); const stack: unknown[] = [json]; - let idx = 0; - while (idx < stack.length) { - const entry = stack[idx++]; + for (let idx = 0; idx < stack.length; idx++) { + const entry = stack[idx]; if (R.is(Object, entry)) { if ((entry as z.core.JSONSchema.BaseSchema).$ref === "#") return true; stack.push(...R.values(entry)); diff --git a/express-zod-api/src/diagnostics.ts b/express-zod-api/src/diagnostics.ts index 91e9c3fd5..c21b602fe 100644 --- a/express-zod-api/src/diagnostics.ts +++ b/express-zod-api/src/diagnostics.ts @@ -29,9 +29,8 @@ export class Diagnostics { const stack: z.core.JSONSchema.BaseSchema[] = [ z.toJSONSchema(endpoint[`${dir}Schema`], { unrepresentable: "any" }), ]; - let idx = 0; - while (idx < stack.length) { - const entry = stack[idx++]; + for (let idx = 0; idx < stack.length; idx++) { + const entry = stack[idx]; if (entry.type && entry.type !== "object") this.logger.warn(`Endpoint ${dir} schema is not object-based`, ctx); for (const prop of ["allOf", "oneOf", "anyOf"] as const) diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts index 8db2674f7..cf9b78c26 100644 --- a/express-zod-api/src/documentation-helpers.ts +++ b/express-zod-api/src/documentation-helpers.ts @@ -371,9 +371,8 @@ const fixReferences = ( ctx: OpenAPIContext, ) => { const stack: unknown[] = [subject, defs]; - let idx = 0; - while (idx < stack.length) { - const entry = stack[idx++]; + for (let idx = 0; idx < stack.length; idx++) { + const entry = stack[idx]; if (R.is(Object, entry)) { if (isReferenceObject(entry) && !entry.$ref.startsWith("#/components")) { const actualName = entry.$ref.split("/").pop()!; diff --git a/express-zod-api/src/json-schema-helpers.ts b/express-zod-api/src/json-schema-helpers.ts index 5076b11d0..3eceb01a1 100644 --- a/express-zod-api/src/json-schema-helpers.ts +++ b/express-zod-api/src/json-schema-helpers.ts @@ -107,9 +107,8 @@ export const flattenIO = ( const stack: Stack = [R.pair(false, jsonSchema)]; // [isOptional, JSON Schema] const flat: FlattenObjectSchema = { type: "object", properties: {} }; const flatRequired: string[] = []; - let idx = 0; - while (idx < stack.length) { - const [isOptional, entry] = stack[idx++]; + for (let idx = 0; idx < stack.length; idx++) { + const [isOptional, entry] = stack[idx]; if (entry.description) flat.description ??= entry.description; stack.push(...processAllOf(entry, mode, isOptional)); stack.push(...processVariants(entry)); diff --git a/express-zod-api/src/routing-walker.ts b/express-zod-api/src/routing-walker.ts index b7e936ea1..27f1177b8 100644 --- a/express-zod-api/src/routing-walker.ts +++ b/express-zod-api/src/routing-walker.ts @@ -91,9 +91,8 @@ export const walkRouting = ({ }: RoutingWalkerParams) => { const stack = processEntries(config, routing); const visited = new Set(); - let idx = 0; - while (idx < stack.length) { - const [path, element, explicitMethod] = stack[idx++]; + for (let idx = 0; idx < stack.length; idx++) { + const [path, element, explicitMethod] = stack[idx]; if (element instanceof AbstractEndpoint) { if (explicitMethod) { checkDuplicate(explicitMethod, path, visited); @@ -111,7 +110,7 @@ export const walkRouting = ({ if (element instanceof ServeStatic) { if (onStatic) element.apply(path, onStatic); } else { - stack.splice(idx, 0, ...processEntries(config, element, path)); + stack.splice(idx + 1, 0, ...processEntries(config, element, path)); } } } From bb95ef4630f73fd86c704413b3939be30dafd15d Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 08:21:29 +0200 Subject: [PATCH 09/16] fix(zts): avoiding negated increment in onTemplateLiteral::readText(). --- express-zod-api/src/zts.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/express-zod-api/src/zts.ts b/express-zod-api/src/zts.ts index ffa505bcc..0b82f8dc2 100644 --- a/express-zod-api/src/zts.ts +++ b/express-zod-api/src/zts.ts @@ -45,11 +45,9 @@ const onTemplateLiteral: Producer = ( const readText = () => { let text = ""; while (idx < parts.length) { - const part = parts[idx++]; - if (isSchema(part)) { - idx--; - break; - } + const part = parts[idx]; + if (isSchema(part)) break; + idx++; text += part ?? ""; // Handle potential undefined values } return text; From f0e93d87825b0c427f627c0a81e26e85b6c9fbea Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 08:58:08 +0200 Subject: [PATCH 10/16] fix(lint): add rule to prohibit shifting. --- eslint.config.js | 4 ++++ express-zod-api/src/integration.ts | 1 + express-zod-api/src/routing-walker.ts | 1 + 3 files changed, 6 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index c0090f571..dd7a46db3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -72,6 +72,10 @@ const performanceConcerns = [ selector: "ImportDeclaration[source.value=/package.json$/]", // #2974 message: "it can not be tree shaken, use tsdown and process.env instead", }, + { + selector: "CallExpression[callee.property.name=/^(shift|unshift|splice)$/]", // #3343 + message: "shifting is 2-20x slower than index-based iteration", + }, ]; const tsFactoryConcerns = [ diff --git a/express-zod-api/src/integration.ts b/express-zod-api/src/integration.ts index 1fe253e34..46f50438c 100644 --- a/express-zod-api/src/integration.ts +++ b/express-zod-api/src/integration.ts @@ -153,6 +153,7 @@ export class Integration extends IntegrationBase { config, onEndpoint: hasHeadMethod ? withHead(onEndpoint) : onEndpoint, }); + // eslint-disable-next-line no-restricted-syntax -- accumulated late, acceptable for generator this.#program.unshift(...this.#aliases.values()); this.#program.push( this.makePathType(), diff --git a/express-zod-api/src/routing-walker.ts b/express-zod-api/src/routing-walker.ts index 27f1177b8..0fcddd13f 100644 --- a/express-zod-api/src/routing-walker.ts +++ b/express-zod-api/src/routing-walker.ts @@ -110,6 +110,7 @@ export const walkRouting = ({ if (element instanceof ServeStatic) { if (onStatic) element.apply(path, onStatic); } else { + // eslint-disable-next-line no-restricted-syntax -- acceptable due to conditional logic stack.splice(idx + 1, 0, ...processEntries(config, element, path)); } } From 0c846ad07095a57d9c4cfcf9667763543c365f99 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:01:54 +0200 Subject: [PATCH 11/16] fix(routing): linear handlers aggregation instead of shifting in initRouting(). --- express-zod-api/src/routing.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/express-zod-api/src/routing.ts b/express-zod-api/src/routing.ts index c9b5d7cea..7011b9ab9 100644 --- a/express-zod-api/src/routing.ts +++ b/express-zod-api/src/routing.ts @@ -89,15 +89,9 @@ export const initRouting = ({ app, config, getLogger, ...rest }: InitProps) => { /** @link https://github.com/RobinTail/express-zod-api/discussions/2791#discussioncomment-13745912 */ if (accessMethods.includes("get")) accessMethods.push("head"); for (const [method, [matchingParsers, endpoint]] of methods) { - const handlers = matchingParsers - .slice() // must be immutable - .concat(async (request, response) => { - const logger = getLogger(request); - return endpoint.execute({ request, response, logger, config }); - }); + const handlers: RequestHandler[] = []; // issue #2706: CORS must go before parsers: if (config.cors) { - // issue #2706, must go before parsers: - handlers.unshift(async (request, response, next) => { + handlers.push(async (request, response, next) => { const logger = getLogger(request); const defaultHeaders = makeCorsHeaders(accessMethods); const headers = @@ -108,6 +102,10 @@ export const initRouting = ({ app, config, getLogger, ...rest }: InitProps) => { next(); }); } + handlers.push(...matchingParsers, async (request, response) => { + const logger = getLogger(request); + return endpoint.execute({ request, response, logger, config }); + }); app[method](path, ...handlers); } if (config.wrongMethodBehavior === 404) continue; From c5e2df43adce2bcff125685a19cee8ddf0629a49 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:12:20 +0200 Subject: [PATCH 12/16] Test for in-depth first logic of walkRouting(). --- express-zod-api/tests/routing-walker.spec.ts | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 express-zod-api/tests/routing-walker.spec.ts diff --git a/express-zod-api/tests/routing-walker.spec.ts b/express-zod-api/tests/routing-walker.spec.ts new file mode 100644 index 000000000..34485686a --- /dev/null +++ b/express-zod-api/tests/routing-walker.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vitest"; +import { defaultEndpointsFactory } from "../src"; +import { walkRouting } from "../src/routing-walker"; + +describe("walkRouting()", () => { + const endpoint = defaultEndpointsFactory.buildVoid({ + handler: vi.fn(), + }); + + test("should process endpoints in depth-first order", () => { + const routing = { + v1: { + user: { retrieve: endpoint, create: endpoint }, + post: endpoint, + }, + }; + + const onEndpoint = vi.fn(); + + walkRouting({ + routing, + config: { cors: false, methodLikeRouteBehavior: "path" }, + onEndpoint, + }); + + expect(onEndpoint.mock.calls).toEqual([ + ["get", "/v1/user/retrieve", endpoint], + ["get", "/v1/user/create", endpoint], + ["get", "/v1/post", endpoint], + ]); + }); + + test("should process nested routes before siblings", () => { + const routing = { a: { b: { c: endpoint }, d: endpoint } }; + + const onEndpoint = vi.fn(); + walkRouting({ + routing, + config: { cors: false, methodLikeRouteBehavior: "path" }, + onEndpoint, + }); + + expect(onEndpoint.mock.calls).toEqual([ + ["get", "/a/b/c", endpoint], + ["get", "/a/d", endpoint], + ]); + }); +}); From 6e44628d3e1b1b1f34d2c64dadf35bf2ce4f7b35 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:13:57 +0200 Subject: [PATCH 13/16] nit: rm splice from eslint rule. --- eslint.config.js | 2 +- express-zod-api/src/routing-walker.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index dd7a46db3..4197ace1c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -73,7 +73,7 @@ const performanceConcerns = [ message: "it can not be tree shaken, use tsdown and process.env instead", }, { - selector: "CallExpression[callee.property.name=/^(shift|unshift|splice)$/]", // #3343 + selector: "CallExpression[callee.property.name=/^(shift|unshift)$/]", // #3343 message: "shifting is 2-20x slower than index-based iteration", }, ]; diff --git a/express-zod-api/src/routing-walker.ts b/express-zod-api/src/routing-walker.ts index 0fcddd13f..27f1177b8 100644 --- a/express-zod-api/src/routing-walker.ts +++ b/express-zod-api/src/routing-walker.ts @@ -110,7 +110,6 @@ export const walkRouting = ({ if (element instanceof ServeStatic) { if (onStatic) element.apply(path, onStatic); } else { - // eslint-disable-next-line no-restricted-syntax -- acceptable due to conditional logic stack.splice(idx + 1, 0, ...processEntries(config, element, path)); } } From dc0ca77b0cca38f9fe0b66e5b6ef304a27fa6aac Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:19:09 +0200 Subject: [PATCH 14/16] rm import from vitest in test. --- express-zod-api/tests/routing-walker.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/express-zod-api/tests/routing-walker.spec.ts b/express-zod-api/tests/routing-walker.spec.ts index 34485686a..b61fca377 100644 --- a/express-zod-api/tests/routing-walker.spec.ts +++ b/express-zod-api/tests/routing-walker.spec.ts @@ -1,4 +1,3 @@ -import { describe, expect, test } from "vitest"; import { defaultEndpointsFactory } from "../src"; import { walkRouting } from "../src/routing-walker"; From 396c11ea740b71de07b554e472fa6c90b43d9ac7 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:20:28 +0200 Subject: [PATCH 15/16] fix(test): rm redundant config option. --- express-zod-api/tests/routing-walker.spec.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/express-zod-api/tests/routing-walker.spec.ts b/express-zod-api/tests/routing-walker.spec.ts index b61fca377..f4fa236ab 100644 --- a/express-zod-api/tests/routing-walker.spec.ts +++ b/express-zod-api/tests/routing-walker.spec.ts @@ -10,22 +10,18 @@ describe("walkRouting()", () => { const routing = { v1: { user: { retrieve: endpoint, create: endpoint }, - post: endpoint, + record: endpoint, }, }; const onEndpoint = vi.fn(); - walkRouting({ - routing, - config: { cors: false, methodLikeRouteBehavior: "path" }, - onEndpoint, - }); + walkRouting({ routing, config: { cors: false }, onEndpoint }); expect(onEndpoint.mock.calls).toEqual([ ["get", "/v1/user/retrieve", endpoint], ["get", "/v1/user/create", endpoint], - ["get", "/v1/post", endpoint], + ["get", "/v1/record", endpoint], ]); }); @@ -33,11 +29,7 @@ describe("walkRouting()", () => { const routing = { a: { b: { c: endpoint }, d: endpoint } }; const onEndpoint = vi.fn(); - walkRouting({ - routing, - config: { cors: false, methodLikeRouteBehavior: "path" }, - onEndpoint, - }); + walkRouting({ routing, config: { cors: false }, onEndpoint }); expect(onEndpoint.mock.calls).toEqual([ ["get", "/a/b/c", endpoint], From 243e0c8077561c8415f627366412118e98981e6c Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 24 Apr 2026 09:26:00 +0200 Subject: [PATCH 16/16] fix(test): using test.each to optimize setup and assertions. --- .../__snapshots__/routing-walker.spec.ts.snap | 36 ++++++++++++++++++ express-zod-api/tests/routing-walker.spec.ts | 38 +++++++------------ 2 files changed, 49 insertions(+), 25 deletions(-) create mode 100644 express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap diff --git a/express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap b/express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap new file mode 100644 index 000000000..260b71ae6 --- /dev/null +++ b/express-zod-api/tests/__snapshots__/routing-walker.spec.ts.snap @@ -0,0 +1,36 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`walkRouting() > should process endpoints in depth-first order 0 1`] = ` +[ + [ + "get", + "/v1/user/retrieve", + Endpoint {}, + ], + [ + "get", + "/v1/user/create", + Endpoint {}, + ], + [ + "get", + "/v1/record", + Endpoint {}, + ], +] +`; + +exports[`walkRouting() > should process endpoints in depth-first order 1 1`] = ` +[ + [ + "get", + "/a/b/c", + Endpoint {}, + ], + [ + "get", + "/a/d", + Endpoint {}, + ], +] +`; diff --git a/express-zod-api/tests/routing-walker.spec.ts b/express-zod-api/tests/routing-walker.spec.ts index f4fa236ab..c10479ddc 100644 --- a/express-zod-api/tests/routing-walker.spec.ts +++ b/express-zod-api/tests/routing-walker.spec.ts @@ -1,4 +1,4 @@ -import { defaultEndpointsFactory } from "../src"; +import { defaultEndpointsFactory, Routing } from "../src"; import { walkRouting } from "../src/routing-walker"; describe("walkRouting()", () => { @@ -6,34 +6,22 @@ describe("walkRouting()", () => { handler: vi.fn(), }); - test("should process endpoints in depth-first order", () => { - const routing = { + const onEndpoint = vi.fn(); + + afterEach(() => { + onEndpoint.mockClear(); + }); + + test.each([ + { v1: { user: { retrieve: endpoint, create: endpoint }, record: endpoint, }, - }; - - const onEndpoint = vi.fn(); - + }, + { a: { b: { c: endpoint }, d: endpoint } }, + ])("should process endpoints in depth-first order %#", (routing) => { walkRouting({ routing, config: { cors: false }, onEndpoint }); - - expect(onEndpoint.mock.calls).toEqual([ - ["get", "/v1/user/retrieve", endpoint], - ["get", "/v1/user/create", endpoint], - ["get", "/v1/record", endpoint], - ]); - }); - - test("should process nested routes before siblings", () => { - const routing = { a: { b: { c: endpoint }, d: endpoint } }; - - const onEndpoint = vi.fn(); - walkRouting({ routing, config: { cors: false }, onEndpoint }); - - expect(onEndpoint.mock.calls).toEqual([ - ["get", "/a/b/c", endpoint], - ["get", "/a/d", endpoint], - ]); + expect(onEndpoint.mock.calls).toMatchSnapshot(); }); });