@routepact/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @routepact/client
2
+
3
+ ky-based HTTP client for `@routepact/core` pacts. Pass a spec and get back a fully-typed response — params, payload, and return type are all inferred automatically.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @routepact/client @routepact/core ky zod
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Create a request function by binding a ky instance and a base URL. You typically do this once and export it for use across your app.
14
+
15
+ ```ts
16
+ import ky from "ky";
17
+ import { createRequest } from "@routepact/client";
18
+
19
+ const api = ky.create({
20
+ headers: { "Content-Type": "application/json" },
21
+ credentials: "include",
22
+ });
23
+
24
+ export const request = createRequest(api, "https://api.example.com");
25
+ ```
26
+
27
+ ## Making requests
28
+
29
+ Pass a spec to `request`. TypeScript infers everything from the spec — what options are required, what the return type is, and whether `params` or `payload` are needed.
30
+
31
+ ```ts
32
+ import { PostPacts } from "../shared/pacts/post.spec";
33
+
34
+ // GET /posts — no options needed
35
+ const { resource: posts, meta } = await request(PostPacts.list);
36
+ // posts: { id: string; title: string }[]
37
+ // meta: { total: number; page: number } | undefined
38
+
39
+ // GET /posts/:id — params are required
40
+ const { resource: post } = await request(PostPacts.getById, {
41
+ params: { id: "abc" },
42
+ });
43
+ // post: { id: string; title: string; body: string }
44
+
45
+ // POST /posts — payload is required, typed from the request schema
46
+ const { resource: created } = await request(PostPacts.create, {
47
+ payload: { title: "Hello", body: "World" },
48
+ });
49
+ // created: { id: string; title: string; body: string }
50
+
51
+ // PATCH /posts/:id — both params and payload
52
+ const { resource: updated } = await request(PostPacts.update, {
53
+ params: { id: "abc" },
54
+ payload: { title: "Updated title" },
55
+ });
56
+
57
+ // DELETE /posts/:id — params required, no response body
58
+ await request(PostPacts.delete, { params: { id: "abc" } });
59
+ ```
60
+
61
+ ## Query parameters
62
+
63
+ Pass arbitrary query params via the `queries` option. They are appended as a query string.
64
+
65
+ ```ts
66
+ const { resource: posts } = await request(PostPacts.list, {
67
+ queries: { page: "2", limit: "20", sort: "createdAt" },
68
+ });
69
+ // → GET /posts?page=2&limit=20&sort=createdAt
70
+ ```
71
+
72
+ ## Customising the ky instance
73
+
74
+ Since `createRequest` accepts any `KyInstance`, you can configure ky however you like before passing it in — hooks, auth headers, retry logic, etc.
75
+
76
+ ```ts
77
+ import ky from "ky";
78
+ import { createRequest } from "@routepact/client";
79
+
80
+ const api = ky.create({
81
+ prefixUrl: "https://api.example.com",
82
+ retry: { limit: 2 },
83
+ hooks: {
84
+ beforeRequest: [
85
+ (request) => {
86
+ request.headers.set("Authorization", `Bearer ${getToken()}`);
87
+ },
88
+ ],
89
+ },
90
+ });
91
+
92
+ export const request = createRequest(api, "");
93
+ // baseUrl is empty because prefixUrl is set on the ky instance
94
+ ```
95
+
96
+ ## Response validation
97
+
98
+ If the spec defines a `response` or `meta` Zod schema, the client validates the data before returning it. If validation fails, an error is thrown:
99
+
100
+ ```
101
+ Error: resource validation failed
102
+ ```
103
+
104
+ The raw `ZodError` is available as `error.cause`.
105
+
106
+ If the spec has no response schema, `resource` is typed as `undefined` and no validation runs.
107
+
108
+ ## Multiple API instances
109
+
110
+ You can create multiple request functions pointing to different APIs:
111
+
112
+ ```ts
113
+ export const internalRequest = createRequest(
114
+ internalKy,
115
+ "https://internal.example.com",
116
+ );
117
+ export const externalRequest = createRequest(
118
+ externalKy,
119
+ "https://api.partner.com",
120
+ );
121
+ ```
122
+
123
+ ## API reference
124
+
125
+ ### `createRequest(kyInstance, baseUrl)`
126
+
127
+ Returns an async function with the signature:
128
+
129
+ ```ts
130
+ <TPact extends RoutePact>(pact: TPact, options?: RouteOptions<TPact>) =>
131
+ Promise<RequestResult<TPact>>;
132
+ ```
133
+
134
+ - `pact` — a `RoutePact` from `@routepact/core`
135
+ - `options.params` — required when the path contains `:param` segments
136
+ - `options.payload` — required for `post`, `patch`, `put` when the spec has a request schema
137
+ - `options.queries` — optional `Record<string, string>` appended as a query string
138
+ - Returns `{ resource, meta? }` typed from the spec's response and meta schemas
@@ -0,0 +1,2 @@
1
+ export * from "./request.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./request.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { type RequestResult, type RouteOptions, type RouteOptionsRequired, type RoutePact } from "@routepact/core";
2
+ import type { KyInstance } from "ky";
3
+ /**
4
+ * Creates a type-safe request function bound to a ky instance and base URL.
5
+ *
6
+ * The returned function accepts a route pact and infers the required options
7
+ * (params, payload, queries) and return type from the pact's Zod schemas.
8
+ *
9
+ * @example
10
+ * const request = createRequest(kyInstance, "https://api.example.com");
11
+ * const result = await request(UserPacts.getById, { params: { id: "123" } });
12
+ */
13
+ export declare function createRequest(apiInstance: KyInstance, baseUrl: string): <TPact extends RoutePact>(pact: TPact, ...[options]: RouteOptionsRequired<TPact> extends true ? [options: RouteOptions<TPact>] : [options?: RouteOptions<TPact>]) => Promise<RequestResult<TPact>>;
14
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,IAAI,CAAC;AAGtD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,IACtD,KAAK,SAAS,SAAS,EACnC,MAAM,KAAK,EACX,GAAG,WAAW,oBAAoB,CAAC,KAAK,CAAC,SAAS,IAAI,GAClD,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,GAC9B,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,KAClC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CA8DjC"}
@@ -0,0 +1,69 @@
1
+ import { buildEndpoint, exhaustiveGuard, } from "@routepact/core";
2
+ /**
3
+ * Creates a type-safe request function bound to a ky instance and base URL.
4
+ *
5
+ * The returned function accepts a route pact and infers the required options
6
+ * (params, payload, queries) and return type from the pact's Zod schemas.
7
+ *
8
+ * @example
9
+ * const request = createRequest(kyInstance, "https://api.example.com");
10
+ * const result = await request(UserPacts.getById, { params: { id: "123" } });
11
+ */
12
+ export function createRequest(apiInstance, baseUrl) {
13
+ return async (pact, ...[options]) => {
14
+ const endpoint = `${baseUrl}${buildEndpoint(pact, options)}`;
15
+ const responseSchema = pact.validation.response;
16
+ const responseMetaSchema = pact.validation.meta;
17
+ let response;
18
+ switch (pact.method) {
19
+ case "get":
20
+ response = apiInstance.get(endpoint);
21
+ break;
22
+ case "post":
23
+ response = apiInstance.post(endpoint, {
24
+ json: { resource: options?.payload ?? {} },
25
+ });
26
+ break;
27
+ case "patch":
28
+ response = apiInstance.patch(endpoint, {
29
+ json: { resource: options?.payload ?? {} },
30
+ });
31
+ break;
32
+ case "put":
33
+ response = apiInstance.put(endpoint, {
34
+ json: { resource: options?.payload ?? {} },
35
+ });
36
+ break;
37
+ case "delete":
38
+ response = apiInstance.delete(endpoint);
39
+ break;
40
+ default:
41
+ exhaustiveGuard(pact.method);
42
+ }
43
+ const data = await response.json();
44
+ if (data.resource && responseSchema) {
45
+ const dataParseResult = responseSchema.safeParse(data.resource);
46
+ if (!dataParseResult.success) {
47
+ throw new Error("resource validation failed", {
48
+ cause: dataParseResult.error,
49
+ });
50
+ }
51
+ let meta;
52
+ if (responseMetaSchema && data.meta) {
53
+ const metaParseResult = responseMetaSchema.safeParse(data.meta);
54
+ if (!metaParseResult.success) {
55
+ throw new Error("meta validation failed", {
56
+ cause: metaParseResult.error,
57
+ });
58
+ }
59
+ meta = metaParseResult.data;
60
+ }
61
+ return {
62
+ resource: dataParseResult.data,
63
+ meta,
64
+ };
65
+ }
66
+ return { resource: undefined };
67
+ };
68
+ }
69
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,GAKhB,MAAM,iBAAiB,CAAC;AAIzB;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,WAAuB,EAAE,OAAe;IACpE,OAAO,KAAK,EACV,IAAW,EACX,GAAG,CAAC,OAAO,CAEwB,EACJ,EAAE;QACjC,MAAM,QAAQ,GAAG,GAAG,OAAO,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;QAE7D,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAChD,MAAM,kBAAkB,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAEhD,IAAI,QAAkC,CAAC;QACvC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,KAAK,KAAK;gBACR,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACrC,MAAM;YACR,KAAK,MAAM;gBACT,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE;oBACpC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE;iBAC3C,CAAC,CAAC;gBACH,MAAM;YACR,KAAK,OAAO;gBACV,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE;oBACrC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE;iBAC3C,CAAC,CAAC;gBACH,MAAM;YACR,KAAK,KAAK;gBACR,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE;oBACnC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE;iBAC3C,CAAC,CAAC;gBACH,MAAM;YACR,KAAK,QAAQ;gBACX,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACxC,MAAM;YACR;gBACE,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA0C,CAAC;QAE3E,IAAI,IAAI,CAAC,QAAQ,IAAI,cAAc,EAAE,CAAC;YACpC,MAAM,eAAe,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAChE,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CAAC,4BAA4B,EAAE;oBAC5C,KAAK,EAAE,eAAe,CAAC,KAAK;iBAC7B,CAAC,CAAC;YACL,CAAC;YAED,IAAI,IAAsD,CAAC;YAC3D,IAAI,kBAAkB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACpC,MAAM,eAAe,GAAG,kBAAkB,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAChE,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC;oBAC7B,MAAM,IAAI,KAAK,CAAC,wBAAwB,EAAE;wBACxC,KAAK,EAAE,eAAe,CAAC,KAAK;qBAC7B,CAAC,CAAC;gBACL,CAAC;gBACD,IAAI,GAAG,eAAe,CAAC,IAA4C,CAAC;YACtE,CAAC;YAED,OAAO;gBACL,QAAQ,EAAE,eAAe,CAAC,IAAI;gBAC9B,IAAI;aACmB,CAAC;QAC5B,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,SAAS,EAA0B,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@routepact/client",
3
+ "version": "0.1.0",
4
+ "description": "Ky-based type-safe HTTP client for route pacts — validates responses against Zod schemas",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -b",
20
+ "dev": "tsc -b --watch"
21
+ },
22
+ "dependencies": {
23
+ "@routepact/core": "0.1.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "peerDependencies": {
29
+ "ky": ">=1.0.0",
30
+ "zod": ">=4.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "ky": "1.14.3",
34
+ "zod": "4.1.7"
35
+ },
36
+ "keywords": [
37
+ "routes",
38
+ "typesafe",
39
+ "ky",
40
+ "zod",
41
+ "client",
42
+ "fetch"
43
+ ],
44
+ "license": "MIT"
45
+ }