@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 +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/request.d.ts +14 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +69 -0
- package/dist/request.js.map +1 -0
- package/package.json +45 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/request.js
ADDED
|
@@ -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
|
+
}
|