Skip to content

Commit 77c82ee

Browse files
feat(v26): method only routes (#3049)
Due to #2938 This will set us free from the requirement to write slash to the keys when we imply method depending routing. Breaking change: `Integration` requires `config` parameter. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `methodLikeRouteBehavior` configuration option to control interpretation of method-like routing keys ("method" or "path") * Added `hasHeadMethod` option for endpoint documentation configuration * **Refactor** * Integration constructor now requires a `config` parameter during initialization * Routing syntax simplified to use cleaner method-based keys <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 547b13f commit 77c82ee

14 files changed

Lines changed: 78 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@
44

55
### v26.0.0
66

7-
- `DependsOnMethod` removed: use flat syntax with explicit method and a slash;
7+
- `DependsOnMethod` removed:
8+
- You can now specify methods as direct keys of an assigned object in `Routing`;
9+
- That object can still contain nested paths as before;
10+
- The keys matching lowercase HTTP methods are treated according to the new config setting `methodLikeRouteBehavior`:
11+
- `method` — when assigned with an Endpoint the key is treated as method of its parent path (default);
12+
- `path` — the key is always treated as a nested path segment;
813
- `options` property renamed to `ctx` in argument of:
914
- `Middleware::handler()`,
1015
- `ResultHandler::handler()`,
1116
- `handler` of `EndpointsFactory::build()` argument,
1217
- `testMiddleware()`;
1318
- `EndpointsFactory::addOptions()` renamed to `addContext()`;
19+
- The `Integration::constructor()` argument object now requires `config` property, similar to `Documentation`;
1420

1521
```patch
1622
const routing: Routing = {
1723
- "/v1/users": new DependsOnMethod({
1824
+ "/v1/users": {
19-
- get: getUserEndpoint,
20-
+ "get /": getUserEndpoint,
25+
get: getUserEndpoint,
2126
- }).nest({
2227
create: makeUserEndpoint
2328
- }),

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,10 @@ const routing: Routing = {
307307
"delete /user/:id": deleteUserEndpoint,
308308
// method-based routing — /v1/account
309309
account: {
310-
"get /": endpointA,
311-
"delete /": endpointA,
312-
"post /": endpointB,
313-
"patch /": endpointB,
310+
get: endpointA,
311+
delete: endpointA,
312+
post: endpointB,
313+
patch: endpointB,
314314
},
315315
},
316316
// static file serving — /public serves files from ./assets
@@ -1087,6 +1087,7 @@ import { Integration } from "express-zod-api";
10871087

10881088
const client = new Integration({
10891089
routing,
1090+
config,
10901091
variant: "client", // <— optional, see also "types" for a DIY solution
10911092
});
10921093

compat-test/migration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { describe, test, expect } from "vitest";
44
describe("Migration", () => {
55
test("should fix the import", async () => {
66
const fixed = await readFile("./sample.ts", "utf-8");
7-
expect(fixed).toBe(`const route = {\n"get /": someEndpoint,\n}\n`);
7+
expect(fixed).toBe(`const route = {\nget: someEndpoint,\n}\n`);
88
});
99
});

example/generate-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ await writeFile(
77
"example.client.ts",
88
await new Integration({
99
routing,
10+
config,
1011
serverUrl: `http://localhost:${config.http!.listen}`,
1112
}).printFormatted(), // or just .print(),
1213
"utf-8",

example/routing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const routing: Routing = {
2020
":id": {
2121
remove: deleteUserEndpoint, // nested path: /v1/user/:id/remove
2222
// syntax 2: methods are defined within the route
23-
"patch /": updateUserEndpoint, // demonstrates authentication
23+
patch: updateUserEndpoint, // demonstrates authentication
2424
},
2525
// demonstrates different response schemas depending on status code
2626
create: createUserEndpoint,

express-zod-api/src/config-type.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export interface CommonConfig {
4646
* @default 405
4747
* */
4848
wrongMethodBehavior?: 404 | 405;
49+
/**
50+
* @desc How to treat Routing keys that look like methods (when assigned with an Endpoint)
51+
* @see Method
52+
* @example "method" — the key is treated as method of its parent path
53+
* @example "path" — the key is treated as a nested path segment
54+
* @default "method"
55+
* */
56+
methodLikeRouteBehavior?: "method" | "path";
4957
/**
5058
* @desc The ResultHandler to use for handling routing, parsing and upload errors
5159
* @default defaultResultHandler

express-zod-api/src/documentation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ interface DocumentationParams {
6262
/**
6363
* @desc Depict the HEAD method for each Endpoint supporting the GET method (feature of Express)
6464
* @default true
65+
* @todo move to config
6566
* */
6667
hasHeadMethod?: boolean;
6768
/** @default inline */
@@ -262,6 +263,7 @@ export class Documentation extends OpenApiBuilder {
262263
};
263264
walkRouting({
264265
routing,
266+
config,
265267
onEndpoint: hasHeadMethod ? withHead(onEndpoint) : onEndpoint,
266268
});
267269
if (tags) this.rootDoc.tags = depictTags(tags);

express-zod-api/src/integration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import { zodToTs } from "./zts.ts";
2323
import { ZTSContext } from "./zts-helpers.ts";
2424
import type Prettier from "prettier";
2525
import { ClientMethod } from "./method.ts";
26+
import { CommonConfig } from "./config-type.ts";
2627

2728
interface IntegrationParams {
2829
routing: Routing;
30+
config: CommonConfig;
2931
/**
3032
* @desc What should be generated
3133
* @example "types" — types of your endpoint requests and responses (for a DIY solution)
@@ -50,6 +52,7 @@ interface IntegrationParams {
5052
/**
5153
* @desc Depict the HEAD method for each Endpoint supporting the GET method (feature of Express)
5254
* @default true
55+
* @todo move to config
5356
* */
5457
hasHeadMethod?: boolean;
5558
/**
@@ -89,6 +92,7 @@ export class Integration extends IntegrationBase {
8992

9093
public constructor({
9194
routing,
95+
config,
9296
brandHandling,
9397
variant = "client",
9498
clientClassName = "Client",
@@ -154,6 +158,7 @@ export class Integration extends IntegrationBase {
154158
};
155159
walkRouting({
156160
routing,
161+
config,
157162
onEndpoint: hasHeadMethod ? withHead(onEndpoint) : onEndpoint,
158163
});
159164
this.#program.unshift(...this.#aliases.values());

express-zod-api/src/routing-walker.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { RoutingError } from "./errors.ts";
33
import { ClientMethod, isMethod, Method } from "./method.ts";
44
import { Routing } from "./routing.ts";
55
import { ServeStatic, StaticHandler } from "./serve-static.ts";
6+
import { CommonConfig } from "./config-type.ts";
67

78
export type OnEndpoint<M extends string = Method> = (
89
method: M,
@@ -20,6 +21,7 @@ export const withHead =
2021

2122
interface RoutingWalkerParams {
2223
routing: Routing;
24+
config: CommonConfig;
2325
onEndpoint: OnEndpoint;
2426
onStatic?: (path: string, handler: StaticHandler) => void;
2527
}
@@ -35,14 +37,23 @@ const detachMethod = (subject: string): [string, Method?] => {
3537
const trimPath = (path: string) =>
3638
path.trim().split("/").filter(Boolean).join("/");
3739

38-
const processEntries = (subject: Routing, parent?: string) =>
39-
Object.entries(subject).map<[string, Routing[string], Method?]>(
40+
const processEntries = (
41+
{ methodLikeRouteBehavior = "method" }: CommonConfig,
42+
subject: Routing,
43+
parent?: string,
44+
) => {
45+
const preferMethod = methodLikeRouteBehavior === "method";
46+
return Object.entries(subject).map<[string, Routing[string], Method?]>(
4047
([_key, item]) => {
41-
const [segment, method] = detachMethod(_key);
48+
const [segment, method] =
49+
isMethod(_key) && preferMethod && item instanceof AbstractEndpoint
50+
? ["/", _key]
51+
: detachMethod(_key);
4252
const path = [parent || ""].concat(trimPath(segment) || []).join("/");
4353
return [path, item, method];
4454
},
4555
);
56+
};
4657

4758
const prohibit = (method: Method, path: string) => {
4859
throw new RoutingError(
@@ -74,10 +85,11 @@ const checkDuplicate = (method: Method, path: string, visited: Set<string>) => {
7485

7586
export const walkRouting = ({
7687
routing,
88+
config,
7789
onEndpoint,
7890
onStatic,
7991
}: RoutingWalkerParams) => {
80-
const stack = processEntries(routing);
92+
const stack = processEntries(config, routing);
8193
const visited = new Set<string>();
8294
while (stack.length) {
8395
const [path, element, explicitMethod] = stack.shift()!;
@@ -98,7 +110,7 @@ export const walkRouting = ({
98110
if (element instanceof ServeStatic) {
99111
if (onStatic) element.apply(path, onStatic);
100112
} else {
101-
stack.unshift(...processEntries(element, path));
113+
stack.unshift(...processEntries(config, element, path));
102114
}
103115
}
104116
}

express-zod-api/src/routing.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import * as R from "ramda";
1616
* @example { "v1/books/:bookId": getBookEndpoint }
1717
* @example { "get /v1/books/:bookId": getBookEndpoint }
1818
* @example { v1: { "patch /books/:bookId": changeBookEndpoint } }
19-
* @example { dependsOnMethod: { "get /": retrieveEndpoint, "post /": createEndpoint } }
19+
* @example { dependsOnMethod: { get: retrieveEndpoint, post: createEndpoint } }
20+
* @see CommonConfig.methodLikeRouteBehavior
2021
* */
2122
export interface Routing {
2223
[K: string]: Routing | AbstractEndpoint | ServeStatic;
@@ -77,7 +78,7 @@ const collectSiblings = ({
7778
familiar.set(path, new Map(config.cors ? [["options", value]] : []));
7879
familiar.get(path)?.set(method, value);
7980
};
80-
walkRouting({ routing, onEndpoint, onStatic: app.use.bind(app) });
81+
walkRouting({ routing, config, onEndpoint, onStatic: app.use.bind(app) });
8182
return familiar;
8283
};
8384

0 commit comments

Comments
 (0)