From a0f6401965b1bc74c47ec34902ac158c810e700e Mon Sep 17 00:00:00 2001 From: Kartik Date: Fri, 10 Jul 2026 15:15:00 +0530 Subject: [PATCH 1/3] test: cover request.params OpenAPI export Add regression coverage and docs clarifying path-parameter export and Chanfana/Zod version support for #286. --- .../request-params-openapi-regression.md | 2 + docs/endpoints/request-validation.md | 8 + tests/integration/openapi-schema.test.ts | 141 +++++++++++++++++- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 .changeset/request-params-openapi-regression.md diff --git a/.changeset/request-params-openapi-regression.md b/.changeset/request-params-openapi-regression.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/request-params-openapi-regression.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/endpoints/request-validation.md b/docs/endpoints/request-validation.md index 0d46eb8..878e0a3 100644 --- a/docs/endpoints/request-validation.md +++ b/docs/endpoints/request-validation.md @@ -145,6 +145,12 @@ Similar to request bodies, Chanfana infers the types of query parameters based o Path parameters are dynamic segments in the URL path, denoted by colons (e.g., `/users/:userId`). Chanfana validates path parameters using Zod schemas defined in `schema.request.params`. +Each key in `request.params` must match a dynamic route segment (for example, `id` for `/users/:id`). When generating the OpenAPI document, Chanfana converts `:id` to `{id}` in the path. Ordinary Zod object properties are exported automatically as required OpenAPI path parameters — you do not need `.openapi({ param: ... })` merely to make them appear. Use `.openapi({ param: ... })` only when you need explicit parameter-level OpenAPI metadata (for example, custom names, references, or other overrides). + +::: tip Chanfana / Zod versions +Chanfana v2 uses Zod 3. Projects on Zod 4 must use Chanfana v3. See the [Migration to Chanfana v3](/migration-to-chanfana-3) guide. +::: + ### Defining Path Parameter Schema with Zod Use `z.object({})` within `schema.request.params` to define the expected path parameters and their validation rules. @@ -178,6 +184,8 @@ class GetProductEndpoint extends OpenAPIRoute { } ``` +Register the endpoint on a matching route such as `/products/:productId`. The generated OpenAPI path will be `/products/{productId}` and will include a required `productId` path parameter derived from the Zod schema. + In this example: * We define a Zod object schema for `schema.request.params` with a single path parameter `productId`. diff --git a/tests/integration/openapi-schema.test.ts b/tests/integration/openapi-schema.test.ts index 64508a4..4226c5a 100644 --- a/tests/integration/openapi-schema.test.ts +++ b/tests/integration/openapi-schema.test.ts @@ -1,9 +1,74 @@ +import { Hono } from "hono"; import { AutoRouter } from "itty-router"; import { describe, expect, it } from "vitest"; -import { fromIttyRouter } from "../../src"; +import { z } from "zod"; +import { fromHono, fromIttyRouter } from "../../src"; +import { OpenAPIRoute } from "../../src/route"; import { ToDoGet, todoRouter } from "../router"; import { buildRequest } from "../utils"; +const userIdParamsSchema = z.object({ + id: z.string().describe("User ID"), +}); + +class GetUserByIdEndpoint extends OpenAPIRoute { + schema = { + tags: ["Users"], + summary: "Get user by ID", + request: { + params: userIdParamsSchema, + }, + responses: { + "200": { + description: "User found", + content: { + "application/json": { + schema: z.object({ + id: z.string(), + name: z.string(), + }), + }, + }, + }, + }, + }; + + async handle() { + return { id: "1", name: "John Doe" }; + } +} + +class GetUserWithQueryEndpoint extends OpenAPIRoute { + schema = { + tags: ["Users"], + summary: "Get user by ID with query", + request: { + params: z.object({ + id: z.string().describe("User ID"), + }), + query: z.object({ + include: z.string().optional().describe("Related resources to include"), + }), + }, + responses: { + "200": { + description: "User found", + content: { + "application/json": { + schema: z.object({ + id: z.string(), + }), + }, + }, + }, + }, + }; + + async handle() { + return { id: "1" }; + } +} + describe("openapi schema", () => { it("custom content type", async () => { const request = await todoRouter.fetch(buildRequest({ method: "GET", path: "/openapi.json" })); @@ -31,4 +96,78 @@ describe("openapi schema", () => { expect(Object.keys(resp.paths)[0]).toEqual("/api/todo"); }); + + it.each([ + { openapiVersion: undefined, openapi: "3.1.0" }, + { openapiVersion: "3" as const, openapi: "3.0.3" }, + ])("exports request.params as OpenAPI path parameters ($openapi)", async ({ openapiVersion, openapi }) => { + const router = fromHono(new Hono(), openapiVersion ? { openapiVersion } : undefined); + router.get("/users/:id", GetUserByIdEndpoint); + + const first = await (await router.fetch(new Request("http://localhost/openapi.json"))).json(); + const second = await (await router.fetch(new Request("http://localhost/openapi.json"))).json(); + + expect(first.openapi).toBe(openapi); + expect(Object.keys(first.paths)).toContain("/users/{id}"); + + const parameters = first.paths["/users/{id}"].get.parameters; + expect(parameters).toHaveLength(1); + expect(parameters[0]).toMatchObject({ + name: "id", + in: "path", + required: true, + schema: { + type: "string", + description: "User ID", + }, + }); + + // Repeated generation stays deterministic and does not mutate the Zod schema. + expect(second.paths["/users/{id}"].get.parameters).toEqual(parameters); + expect(userIdParamsSchema.shape.id.description).toBe("User ID"); + }); + + it("keeps query parameters alongside path parameters without duplicates", async () => { + const router = fromHono(new Hono()); + router.get("/users/:id", GetUserWithQueryEndpoint); + + const schema = await (await router.fetch(new Request("http://localhost/openapi.json"))).json(); + const parameters = schema.paths["/users/{id}"].get.parameters; + + expect(parameters).toHaveLength(2); + expect(parameters.filter((parameter: { name: string }) => parameter.name === "id")).toHaveLength(1); + expect(parameters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "id", + in: "path", + required: true, + schema: expect.objectContaining({ type: "string" }), + }), + expect.objectContaining({ + name: "include", + in: "query", + schema: expect.objectContaining({ type: "string" }), + }), + ]), + ); + }); + + it("exports request.params for itty-router through the shared registration layer", async () => { + const router = fromIttyRouter(AutoRouter()); + router.get("/users/:id", GetUserByIdEndpoint); + + const schema = await (await router.fetch(buildRequest({ method: "GET", path: "/openapi.json" }))).json(); + const parameters = schema.paths["/users/{id}"].get.parameters; + + expect(parameters).toHaveLength(1); + expect(parameters[0]).toMatchObject({ + name: "id", + in: "path", + required: true, + schema: { + type: "string", + }, + }); + }); }); From 252432cf634ef039fd4ceebaaab7e8e5f7c5198f Mon Sep 17 00:00:00 2001 From: Kartik Date: Fri, 10 Jul 2026 15:33:52 +0530 Subject: [PATCH 2/3] fix: tighten path-param docs and unblock template CI Correct OpenAPI param metadata guidance, require non-optional path schemas, exercise the public OpenAPIRoute export, and pin template wrangler to 4.107.1 so npm install stays compatible with workers-types v4. --- docs/endpoints/request-validation.md | 4 +++- template/package.json | 2 +- tests/integration/openapi-schema.test.ts | 9 ++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/endpoints/request-validation.md b/docs/endpoints/request-validation.md index 878e0a3..a99f6a9 100644 --- a/docs/endpoints/request-validation.md +++ b/docs/endpoints/request-validation.md @@ -145,7 +145,9 @@ Similar to request bodies, Chanfana infers the types of query parameters based o Path parameters are dynamic segments in the URL path, denoted by colons (e.g., `/users/:userId`). Chanfana validates path parameters using Zod schemas defined in `schema.request.params`. -Each key in `request.params` must match a dynamic route segment (for example, `id` for `/users/:id`). When generating the OpenAPI document, Chanfana converts `:id` to `{id}` in the path. Ordinary Zod object properties are exported automatically as required OpenAPI path parameters — you do not need `.openapi({ param: ... })` merely to make them appear. Use `.openapi({ param: ... })` only when you need explicit parameter-level OpenAPI metadata (for example, custom names, references, or other overrides). +Each key in `request.params` must match a dynamic route segment (for example, `id` for `/users/:id`). When generating the OpenAPI document, Chanfana converts `:id` to `{id}` in the path. Ordinary Zod object properties are exported automatically as OpenAPI parameters, so `.openapi({ param: ... })` is not required merely to make them appear. Use `param` metadata only for Parameter Object fields such as a parameter-level description, deprecation status, examples, or serialization options. Do not override `name` or `in`; they are derived from the object key and `request.params`. + +Path-parameter schemas must not be optional or nullable: OpenAPI requires every path parameter to have `required: true`. ::: tip Chanfana / Zod versions Chanfana v2 uses Zod 3. Projects on Zod 4 must use Chanfana v3. See the [Migration to Chanfana v3](/migration-to-chanfana-3) guide. diff --git a/template/package.json b/template/package.json index 4d2703d..294258c 100644 --- a/template/package.json +++ b/template/package.json @@ -20,6 +20,6 @@ "@cloudflare/workers-types": "^4.20260303.0", "typescript": "^5.9.0", "vitest": "^3.2.0", - "wrangler": "^4.68.0" + "wrangler": "4.107.1" } } diff --git a/tests/integration/openapi-schema.test.ts b/tests/integration/openapi-schema.test.ts index 4226c5a..fd8dbfb 100644 --- a/tests/integration/openapi-schema.test.ts +++ b/tests/integration/openapi-schema.test.ts @@ -2,8 +2,7 @@ import { Hono } from "hono"; import { AutoRouter } from "itty-router"; import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { fromHono, fromIttyRouter } from "../../src"; -import { OpenAPIRoute } from "../../src/route"; +import { fromHono, fromIttyRouter, OpenAPIRoute } from "../../src"; import { ToDoGet, todoRouter } from "../router"; import { buildRequest } from "../utils"; @@ -98,10 +97,10 @@ describe("openapi schema", () => { }); it.each([ - { openapiVersion: undefined, openapi: "3.1.0" }, + { openapiVersion: "3.1" as const, openapi: "3.1.0" }, { openapiVersion: "3" as const, openapi: "3.0.3" }, ])("exports request.params as OpenAPI path parameters ($openapi)", async ({ openapiVersion, openapi }) => { - const router = fromHono(new Hono(), openapiVersion ? { openapiVersion } : undefined); + const router = fromHono(new Hono(), { openapiVersion }); router.get("/users/:id", GetUserByIdEndpoint); const first = await (await router.fetch(new Request("http://localhost/openapi.json"))).json(); @@ -122,7 +121,7 @@ describe("openapi schema", () => { }, }); - // Repeated generation stays deterministic and does not mutate the Zod schema. + // Repeated generation stays deterministic and preserves schema metadata. expect(second.paths["/users/{id}"].get.parameters).toEqual(parameters); expect(userIdParamsSchema.shape.id.description).toBe("User ID"); }); From c77dbd3995f677637e3107d224ad040e55bebdbc Mon Sep 17 00:00:00 2001 From: Kartik Date: Fri, 10 Jul 2026 15:51:04 +0530 Subject: [PATCH 3/3] chore: declare template Node 22 and monitor template deps Add engines/README prerequisites for Wrangler and Dependabot coverage for /template so the pinned wrangler line stays maintainable. --- .github/dependabot.yml | 8 ++++++++ template/AGENTS.md | 2 ++ template/README.md | 4 ++++ template/package.json | 3 +++ 4 files changed, 17 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e167082..b387b1d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,14 @@ updates: dependency-type: "production" dev-deps: dependency-type: "development" + - package-ecosystem: "npm" + directory: "/template" + schedule: + interval: "weekly" + groups: + template-deps: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/template/AGENTS.md b/template/AGENTS.md index 51a0f98..282da96 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -2,6 +2,8 @@ A starter Cloudflare Worker using Chanfana (OpenAPI), Hono, D1, and Zod v4. This is a copyable template — users clone it and build on top. +Requires **Node.js 22 or later** (Wrangler engines constraint). + ## Commands ```bash diff --git a/template/README.md b/template/README.md index 5ea17a2..72100f1 100644 --- a/template/README.md +++ b/template/README.md @@ -8,6 +8,10 @@ Scaffold a new project with the `create-cloudflare` CLI: npm create cloudflare@latest -- --template https://github.com/cloudflare/chanfana/tree/main/template ``` +## Prerequisites + +- **Node.js 22 or later** (required by Wrangler) + ## Setup ```bash diff --git a/template/package.json b/template/package.json index 294258c..89a1ae4 100644 --- a/template/package.json +++ b/template/package.json @@ -2,6 +2,9 @@ "name": "chanfana-template", "version": "0.0.0", "private": true, + "engines": { + "node": ">=22" + }, "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy",