klaim 1.12.48 → 1.12.49
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/context7.json +4 -0
- package/deno.json +1 -1
- package/docs/codedocs/api-and-routes.md +120 -0
- package/docs/codedocs/api-reference/api.md +143 -0
- package/docs/codedocs/api-reference/cache.md +84 -0
- package/docs/codedocs/api-reference/errors.md +113 -0
- package/docs/codedocs/api-reference/group.md +107 -0
- package/docs/codedocs/api-reference/hook.md +78 -0
- package/docs/codedocs/api-reference/klaim.md +105 -0
- package/docs/codedocs/api-reference/registry.md +109 -0
- package/docs/codedocs/api-reference/route.md +165 -0
- package/docs/codedocs/architecture.md +129 -0
- package/docs/codedocs/groups-and-hierarchy.md +110 -0
- package/docs/codedocs/guides/advanced-runtime-patterns.md +152 -0
- package/docs/codedocs/guides/defining-a-client.md +137 -0
- package/docs/codedocs/guides/pagination-validation-and-observability.md +110 -0
- package/docs/codedocs/index.md +128 -0
- package/docs/codedocs/request-lifecycle.md +130 -0
- package/docs/codedocs/resilience-and-control.md +123 -0
- package/docs/codedocs/types.md +187 -0
- package/package.json +8 -8
- package/tests/04.klaim.test.ts +15 -1
- package/tests/05.cache.test.ts +11 -1
- package/tests/06.retry.test.ts +9 -1
- package/tests/07.validate.test.ts +13 -1
- package/tests/08.group.test.ts +9 -1
- package/tests/09.pagination.test.ts +19 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Klaim Runtime"
|
|
3
|
+
description: "Reference for the exported `Klaim` runtime object and the dynamic route call shapes it exposes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Source: `src/core/Klaim.ts`
|
|
7
|
+
|
|
8
|
+
Import path:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { Klaim } from "klaim";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`Klaim` is the exported runtime object that receives APIs, groups, and route functions as you register them.
|
|
15
|
+
|
|
16
|
+
## Signature
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
export const Klaim: IApiReference = {};
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Supporting exported types from the same source file:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
export type IArgs = Record<string, unknown>;
|
|
26
|
+
export type IBody = Record<string, unknown>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Internally, route handlers are created with these shapes:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
type RouteFunction<T = any> = {
|
|
33
|
+
(offset?: number, args?: IArgs, body?: IBody): Promise<T>;
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That `RouteFunction` type is not exported from the package root, but it explains how generated route functions behave.
|
|
38
|
+
|
|
39
|
+
## How Calls Work
|
|
40
|
+
|
|
41
|
+
For non-paginated routes:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
await Klaim.api.route<T>(args?, body?);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For paginated routes:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
await Klaim.api.route<T>(offset?, args?, body?);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`createRouteHandler()` decides which call form to use by checking `element.pagination`.
|
|
54
|
+
|
|
55
|
+
## Common Patterns
|
|
56
|
+
|
|
57
|
+
### Basic route call
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
Api.create("todos", "https://jsonplaceholder.typicode.com", () => {
|
|
61
|
+
Route.get("getOne", "/todos/[id]");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const todo = await Klaim.todos.getOne({ id: 1 });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Route call with body
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
Api.create("posts", "https://jsonplaceholder.typicode.com", () => {
|
|
71
|
+
Route.post("create", "/posts");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const created = await Klaim.posts.create(
|
|
75
|
+
{},
|
|
76
|
+
{ title: "Hello", body: "From Klaim", userId: 1 }
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Paginated route call
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
Api.create("pokemon", "https://pokeapi.co/api/v2", () => {
|
|
84
|
+
Route.get("list", "/pokemon").withPagination({
|
|
85
|
+
pageParam: "offset",
|
|
86
|
+
limitParam: "limit",
|
|
87
|
+
limit: 5,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const firstPage = await Klaim.pokemon.list(0);
|
|
92
|
+
const secondPage = await Klaim.pokemon.list(5);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Notes on Dynamic Structure
|
|
96
|
+
|
|
97
|
+
`Klaim` starts as an empty object. `Registry.registerElement()` adds nested objects for APIs and groups, and `Registry.addToKlaimRoute()` writes route functions into the correct nested location.
|
|
98
|
+
|
|
99
|
+
That means:
|
|
100
|
+
|
|
101
|
+
- The object shape is determined entirely by registration order and names.
|
|
102
|
+
- `Registry.reset()` can clear the object back to empty.
|
|
103
|
+
- Property names are normalized to camelCase during registration.
|
|
104
|
+
|
|
105
|
+
Related pages: [Registry](/docs/api-reference/registry), [Request Lifecycle](/docs/request-lifecycle), [Api](/docs/api-reference/api)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Registry"
|
|
3
|
+
description: "Reference for the exported `Registry` singleton that stores Klaim declarations and builds the runtime tree."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Source: `src/core/Registry.ts`
|
|
7
|
+
|
|
8
|
+
Import path:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { Registry } from "klaim";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`Registry` is the internal data store that keeps every API, group, and route element and mirrors that structure onto `Klaim`. It is exported, so you can use it in tests, debugging tools, or advanced runtime extensions.
|
|
15
|
+
|
|
16
|
+
## Signatures
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
class Registry {
|
|
20
|
+
static get i(): Registry;
|
|
21
|
+
|
|
22
|
+
registerElement(element: IElement): void;
|
|
23
|
+
getCurrentParent(): IElement | null;
|
|
24
|
+
setCurrentParent(fullPath: string): void;
|
|
25
|
+
clearCurrentParent(): void;
|
|
26
|
+
registerRoute(element: IElement): void;
|
|
27
|
+
getElementKey(element: IElement): string;
|
|
28
|
+
getFullPath(element: IElement): string;
|
|
29
|
+
getRoute(apiName: string, routeName: string): IElement | undefined;
|
|
30
|
+
getChildren(elementPath: string): IElement[];
|
|
31
|
+
static updateElement(element: IElement): IElement;
|
|
32
|
+
getApi(name: string): IElement | undefined;
|
|
33
|
+
reset(): void;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Core Methods
|
|
38
|
+
|
|
39
|
+
### `Registry.i`
|
|
40
|
+
|
|
41
|
+
The singleton instance getter.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const registry = Registry.i;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `getRoute(apiName, routeName)`
|
|
50
|
+
|
|
51
|
+
Looks up a route by its API name and route name.
|
|
52
|
+
|
|
53
|
+
| Parameter | Type | Default | Description |
|
|
54
|
+
|-----------|------|---------|-------------|
|
|
55
|
+
| `apiName` | `string` | — | API name used in the registry key. |
|
|
56
|
+
| `routeName` | `string` | — | Route name used in the registry key. |
|
|
57
|
+
|
|
58
|
+
Returns: `IElement | undefined`
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const route = Registry.i.getRoute("inventory", "listProducts");
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `getApi(name)`
|
|
67
|
+
|
|
68
|
+
Looks up an API by name. If the name is nested under groups, the method searches registry keys that end with `.${name}`.
|
|
69
|
+
|
|
70
|
+
Returns: `IElement | undefined`
|
|
71
|
+
|
|
72
|
+
### `getChildren(elementPath)`
|
|
73
|
+
|
|
74
|
+
Returns every element whose `parent` exactly matches the provided path.
|
|
75
|
+
|
|
76
|
+
Returns: `IElement[]`
|
|
77
|
+
|
|
78
|
+
### `reset()`
|
|
79
|
+
|
|
80
|
+
Clears the internal registry map and deletes every property from the exported `Klaim` object. This is mainly useful in tests.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
Registry.i.reset();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Registration Methods
|
|
89
|
+
|
|
90
|
+
`registerElement()` is used for APIs and groups. `registerRoute()` is used for routes and throws if no current parent is set. These methods are usually called by `Api.create()`, `Group.create()`, and `Route.*()` for you.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
Api.create("service", "https://example.com", () => {
|
|
96
|
+
Route.get("health", "/health");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(Registry.i.getRoute("service", "health"));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## When to Use the Registry Directly
|
|
103
|
+
|
|
104
|
+
- Inspect declarations in tests
|
|
105
|
+
- Attach callbacks after registration
|
|
106
|
+
- Clear global state between isolated runs
|
|
107
|
+
- Build custom debug tooling around registered elements
|
|
108
|
+
|
|
109
|
+
Related pages: [Klaim Runtime](/docs/api-reference/klaim), [Hook](/docs/api-reference/hook), [Types](/docs/types)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Route"
|
|
3
|
+
description: "Reference for the exported `Route` class, HTTP helpers, and validation support."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Source: `src/core/Route.ts`
|
|
7
|
+
|
|
8
|
+
Import path:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { Route } from "klaim";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`Route` defines one endpoint inside an API or group context. It carries the HTTP method, path, header overrides, detected path arguments, and optional runtime controls such as validation, retry, and timeout.
|
|
15
|
+
|
|
16
|
+
## Signatures
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
class Route extends Element {
|
|
20
|
+
constructor(
|
|
21
|
+
name: string,
|
|
22
|
+
url: string,
|
|
23
|
+
headers: IHeaders = {},
|
|
24
|
+
method: RouteMethod = RouteMethod.GET
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
static get(name: string, url: string, headers: IHeaders = {}): Element;
|
|
28
|
+
static post(name: string, url: string, headers: IHeaders = {}): Element;
|
|
29
|
+
static put(name: string, url: string, headers: IHeaders = {}): Element;
|
|
30
|
+
static delete(name: string, url: string, headers: IHeaders = {}): Element;
|
|
31
|
+
static patch(name: string, url: string, headers: IHeaders = {}): Element;
|
|
32
|
+
static options(name: string, url: string, headers: IHeaders = {}): Element;
|
|
33
|
+
|
|
34
|
+
validate(schema: { validate: (data: unknown) => Promise<unknown> }): Element;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Inherited chainable methods:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
before(callback: ICallback<ICallbackBeforeArgs>): this
|
|
42
|
+
after(callback: ICallback<ICallbackAfterArgs>): this
|
|
43
|
+
onCall(callback: ICallback<ICallbackCallArgs>): this
|
|
44
|
+
withCache(duration: number = 20): this
|
|
45
|
+
withRetry(maxRetries: number = 2): this
|
|
46
|
+
withPagination(config: IPaginationConfig = {}): this
|
|
47
|
+
withRate(config: Partial<IRateLimitConfig> = {}): this
|
|
48
|
+
withTimeout(duration: number = 5, message: string = "Request timed out"): this
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Constructor
|
|
52
|
+
|
|
53
|
+
Most users should prefer the static helpers because they register routes automatically, but the public constructor exists.
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Default | Description |
|
|
56
|
+
|-----------|------|---------|-------------|
|
|
57
|
+
| `name` | `string` | — | Route name, normalized to camelCase by `Element`. |
|
|
58
|
+
| `url` | `string` | — | Route path fragment, scanned for `[param]` placeholders. |
|
|
59
|
+
| `headers` | `Record<string, string>` | `{}` | Route-level header overrides. |
|
|
60
|
+
| `method` | `RouteMethod` | `GET` | HTTP method stored on the route element. |
|
|
61
|
+
|
|
62
|
+
Returns: `Route`
|
|
63
|
+
|
|
64
|
+
## Static Route Helpers
|
|
65
|
+
|
|
66
|
+
All six helpers call the same private `createRoute()` function and differ only by the HTTP method they assign.
|
|
67
|
+
|
|
68
|
+
| Method | Signature | Description |
|
|
69
|
+
|--------|-----------|-------------|
|
|
70
|
+
| `get` | `Route.get(name, url, headers?)` | Register a GET route. |
|
|
71
|
+
| `post` | `Route.post(name, url, headers?)` | Register a POST route. |
|
|
72
|
+
| `put` | `Route.put(name, url, headers?)` | Register a PUT route. |
|
|
73
|
+
| `delete` | `Route.delete(name, url, headers?)` | Register a DELETE route. |
|
|
74
|
+
| `patch` | `Route.patch(name, url, headers?)` | Register a PATCH route. |
|
|
75
|
+
| `options` | `Route.options(name, url, headers?)` | Register an OPTIONS route. |
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
Api.create("users", "https://example.com", () => {
|
|
81
|
+
Route.get("list", "/users");
|
|
82
|
+
Route.get("getOne", "/users/[id]");
|
|
83
|
+
Route.post("create", "/users", { Authorization: "Bearer token" });
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## `validate(schema)`
|
|
88
|
+
|
|
89
|
+
Adds a response validator. The schema object only needs a `validate()` method that returns a promise, which is why Yup works out of the box in the tests.
|
|
90
|
+
|
|
91
|
+
| Parameter | Type | Default | Description |
|
|
92
|
+
|-----------|------|---------|-------------|
|
|
93
|
+
| `schema` | `{ validate: (data: unknown) => Promise<unknown> }` | — | Async validation or coercion schema. |
|
|
94
|
+
|
|
95
|
+
Returns: `Element`
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import * as yup from "yup";
|
|
101
|
+
|
|
102
|
+
const schema = yup.object({
|
|
103
|
+
id: yup.number().required(),
|
|
104
|
+
title: yup.string().required(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Api.create("posts", "https://jsonplaceholder.typicode.com", () => {
|
|
108
|
+
Route.get("getOne", "/posts/[id]").validate(schema);
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Runtime Controls
|
|
113
|
+
|
|
114
|
+
### `before(callback)` and `after(callback)`
|
|
115
|
+
|
|
116
|
+
These hooks are executed directly by `applyBefore()` and `applyAfter()` in `src/core/Klaim.ts`.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
Route.get("profile", "/me").before(({ route, api, url, config }) => {
|
|
122
|
+
return {
|
|
123
|
+
route,
|
|
124
|
+
api,
|
|
125
|
+
url,
|
|
126
|
+
config: {
|
|
127
|
+
...config,
|
|
128
|
+
headers: {
|
|
129
|
+
...(config.headers as Record<string, string>),
|
|
130
|
+
Authorization: `Bearer ${token}`,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `withPagination(config?)`
|
|
138
|
+
|
|
139
|
+
Marks the route as paginated. The generated route handler then expects the first argument to be the page or offset number.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
Route.get("list", "/pokemon").withPagination({
|
|
145
|
+
pageParam: "offset",
|
|
146
|
+
limitParam: "limit",
|
|
147
|
+
limit: 20,
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `withRate(config?)`, `withRetry(maxRetries?)`, `withTimeout(duration?, message?)`, `withCache(duration?)`
|
|
152
|
+
|
|
153
|
+
These settings are enforced inside `fetchWithRetry()` and `fetchWithCache()`.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
Route.get("expensive", "/reports/[id]")
|
|
159
|
+
.withRate({ limit: 2, duration: 60 })
|
|
160
|
+
.withRetry(1)
|
|
161
|
+
.withTimeout(5)
|
|
162
|
+
.withCache(120);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Related pages: [Api](/docs/api-reference/api), [Klaim Runtime](/docs/api-reference/klaim), [Types](/docs/types)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Architecture"
|
|
3
|
+
description: "Understand how Klaim registers APIs, builds the runtime object, and executes requests."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Klaim is built around a small set of runtime primitives: `Element` for shared configuration, `Api` and `Route` for declaration, `Group` for hierarchy, `Registry` for storage, and `Klaim` for execution. The codebase is compact, and almost all user-facing behavior flows through `src/core/Klaim.ts` and `src/core/Registry.ts`.
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
graph TD
|
|
10
|
+
A[Api.create / Group.create / Route.get] --> B[Element instances]
|
|
11
|
+
B --> C[Registry registerElement/registerRoute]
|
|
12
|
+
C --> D[Klaim object tree]
|
|
13
|
+
D --> E[createRouteHandler]
|
|
14
|
+
E --> F[callApi]
|
|
15
|
+
F --> G[applyArgs]
|
|
16
|
+
F --> H[applyBefore]
|
|
17
|
+
F --> I[fetchWithRetry]
|
|
18
|
+
I --> J[rateLimit.ts]
|
|
19
|
+
I --> K[fetchWithCache.ts]
|
|
20
|
+
I --> L[timeout.ts]
|
|
21
|
+
F --> M[schema.validate]
|
|
22
|
+
F --> N[applyAfter]
|
|
23
|
+
F --> O[Hook.run]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Module Roles
|
|
27
|
+
|
|
28
|
+
`src/core/Element.ts` is the common base class. It owns the shared state for all declared elements: `name`, `url`, `headers`, callback slots, cache settings, retry settings, rate limiting, timeout, pagination, and validation metadata. Every other core class inherits these chainable configuration methods from `Element`.
|
|
29
|
+
|
|
30
|
+
`src/core/Api.ts` and `src/core/Route.ts` are declaration helpers. `Api.create()` creates an API node and temporarily changes the current parent in the registry so route declarations inside its callback register under that API. `Route.get()`, `Route.post()`, and the other static helpers create route nodes and register them under the current parent.
|
|
31
|
+
|
|
32
|
+
`src/core/Group.ts` adds hierarchy. A group can sit above APIs or above routes, depending on where it is declared. After `Group.create()` finishes its callback, group-level `withCache()`, `withRetry()`, `withTimeout()`, `before()`, `after()`, and `onCall()` copy settings down to already-registered children.
|
|
33
|
+
|
|
34
|
+
`src/core/Registry.ts` is the structural center of the library. It stores every element in `_elements`, tracks the current declaration parent in `_currentParent`, and mutates the exported `Klaim` object so that each route becomes a callable function at the correct nested property path.
|
|
35
|
+
|
|
36
|
+
`src/core/Klaim.ts` is the runtime center. `createRouteHandler()` builds the public callable functions placed on the `Klaim` object. `callApi()` resolves the owning API, fills route arguments, adds pagination query parameters when configured, applies middleware, executes the request with retry and timeout handling, validates the response, and then fires hook callbacks.
|
|
37
|
+
|
|
38
|
+
## Key Design Decisions
|
|
39
|
+
|
|
40
|
+
### Declarations are runtime-driven, not generated
|
|
41
|
+
|
|
42
|
+
Klaim does not generate a client from an OpenAPI schema or compile route definitions into files. Instead, it builds the client in memory by mutating the exported `Klaim` object at registration time. You can see this in `Registry.registerElement()` and `Registry.addToKlaimRoute()` inside `src/core/Registry.ts`.
|
|
43
|
+
|
|
44
|
+
Why this choice matters:
|
|
45
|
+
|
|
46
|
+
- You can declare APIs dynamically from normal application code.
|
|
47
|
+
- Nested groups are just nested object paths, so the runtime shape is easy to inspect.
|
|
48
|
+
- The trade-off is that type inference is limited compared with code generation, because the object shape is created at runtime.
|
|
49
|
+
|
|
50
|
+
### Configuration lives on elements
|
|
51
|
+
|
|
52
|
+
The `Element` base class in `src/core/Element.ts` keeps configuration local to each API, route, or group. That makes chainable declarations easy:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
Route.get("list", "/users")
|
|
56
|
+
.withRetry(3)
|
|
57
|
+
.withTimeout(2)
|
|
58
|
+
.withRate({ limit: 10, duration: 60 });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This design keeps the public API small, but it also means the runtime has to decide which level wins when both API and route settings exist. In `fetchWithRetry()`, route-level retry, timeout, and rate limit take precedence; API-level values are fallback values.
|
|
62
|
+
|
|
63
|
+
### The middleware model is single-slot, not a chain
|
|
64
|
+
|
|
65
|
+
`Element.callbacks` stores one `before`, one `after`, and one `call` callback. There is no internal array and no middleware composition pipeline. `callApi()` invokes `route.callbacks.before` and `route.callbacks.after`; `fetchWithRetry()` invokes `route.callbacks.call` or, if it is missing, `api.callbacks.call`.
|
|
66
|
+
|
|
67
|
+
Why this is important:
|
|
68
|
+
|
|
69
|
+
- The behavior is predictable and cheap.
|
|
70
|
+
- Overwriting a callback is easy to reason about.
|
|
71
|
+
- You cannot register multiple `before` handlers for the same route and expect them all to run.
|
|
72
|
+
|
|
73
|
+
## Request Lifecycle
|
|
74
|
+
|
|
75
|
+
```mermaid
|
|
76
|
+
sequenceDiagram
|
|
77
|
+
participant U as User Code
|
|
78
|
+
participant K as Klaim route handler
|
|
79
|
+
participant R as Registry
|
|
80
|
+
participant F as callApi/fetchWithRetry
|
|
81
|
+
participant N as Network
|
|
82
|
+
U->>K: Klaim.api.route(args, body)
|
|
83
|
+
K->>F: callApi(parent, route, ...)
|
|
84
|
+
F->>R: getApi(...)
|
|
85
|
+
F->>F: applyArgs + pagination
|
|
86
|
+
F->>F: route before callback
|
|
87
|
+
F->>F: rate limit / retry / timeout
|
|
88
|
+
F->>N: fetch(...)
|
|
89
|
+
N-->>F: JSON response
|
|
90
|
+
F->>F: schema.validate(...)
|
|
91
|
+
F->>F: route after callback
|
|
92
|
+
F->>F: Hook.run("api.route")
|
|
93
|
+
F-->>U: parsed data
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The lifecycle is deliberately linear:
|
|
97
|
+
|
|
98
|
+
1. Resolve the API owner for the route.
|
|
99
|
+
2. Build the final URL from `api.url` and `route.url`.
|
|
100
|
+
3. Replace `[param]` placeholders with values from `args`.
|
|
101
|
+
4. Add pagination query parameters if `route.pagination` is defined.
|
|
102
|
+
5. Build the fetch config with merged headers and a JSON body for non-GET requests.
|
|
103
|
+
6. Run the route-level `before` callback if present.
|
|
104
|
+
7. Execute the request through `fetchWithRetry()`, which also handles rate limits and timeouts.
|
|
105
|
+
8. Run schema validation if `route.schema` exists.
|
|
106
|
+
9. Run the route-level `after` callback if present.
|
|
107
|
+
10. Trigger `Hook.run()` for the route name.
|
|
108
|
+
|
|
109
|
+
## How the Pieces Fit Together
|
|
110
|
+
|
|
111
|
+
The usual declaration flow is:
|
|
112
|
+
|
|
113
|
+
1. `Api.create("service", "...", () => { ... })`
|
|
114
|
+
2. `Registry.setCurrentParent("service")`
|
|
115
|
+
3. `Route.get("list", "/items")`
|
|
116
|
+
4. `Registry.registerRoute(route)`
|
|
117
|
+
5. `Registry.addToKlaimRoute(route)`
|
|
118
|
+
6. `Klaim.service.list()` becomes callable
|
|
119
|
+
|
|
120
|
+
Groups introduce one extra parent path layer, but the principle stays the same. The registry always stores a dot-path such as `shop.catalog.products.list`, and the `Klaim` object mirrors that structure.
|
|
121
|
+
|
|
122
|
+
Important implementation details to keep in mind while reading the rest of the docs:
|
|
123
|
+
|
|
124
|
+
- Names are normalized with `toCamelCase()` from `src/tools/toCamelCase.ts`.
|
|
125
|
+
- URLs are normalized with `cleanUrl()` from `src/tools/cleanUrl.ts`, which trims only leading and trailing slashes.
|
|
126
|
+
- Cache storage is global and in-memory through the `Cache` singleton in `src/core/Cache.ts`.
|
|
127
|
+
- Rate limiting is global and in-memory through the `requestLogs` map in `src/tools/rateLimit.ts`.
|
|
128
|
+
|
|
129
|
+
From here, the most useful next pages are [APIs and Routes](/docs/api-and-routes), [Groups and Hierarchy](/docs/groups-and-hierarchy), and [Request Lifecycle](/docs/request-lifecycle).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Groups and Hierarchy"
|
|
3
|
+
description: "See how Klaim groups APIs and routes into nested namespaces with inherited settings."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Groups are Klaim’s namespace and inheritance mechanism. They let you organize routes inside an API, organize multiple APIs under a shared namespace, and apply some settings to all children after registration.
|
|
7
|
+
|
|
8
|
+
## What a Group Is
|
|
9
|
+
|
|
10
|
+
A `Group` is an `Element` with `type: "group"` created by `Group.create()` in `src/core/Group.ts`. Unlike an API, it has no real base URL. Its job is structural:
|
|
11
|
+
|
|
12
|
+
- When declared inside an API callback, it creates a nested route namespace.
|
|
13
|
+
- When declared at the root level, it can hold multiple APIs.
|
|
14
|
+
- When declared inside another group, it creates deeper nesting.
|
|
15
|
+
|
|
16
|
+
Because `Registry.registerElement()` treats groups the same way it treats APIs for object creation, both appear as nested objects on `Klaim`.
|
|
17
|
+
|
|
18
|
+
## Why Groups Exist
|
|
19
|
+
|
|
20
|
+
Groups solve two practical problems:
|
|
21
|
+
|
|
22
|
+
- Large API surfaces become navigable because related routes live under one subtree.
|
|
23
|
+
- Shared behavior such as retries, timeout, or a request hook can be copied to all children in one place.
|
|
24
|
+
|
|
25
|
+
This is especially useful when a service has feature areas with different runtime needs, such as long-lived cached catalog routes and short-timeout admin routes.
|
|
26
|
+
|
|
27
|
+
## How Groups Work Internally
|
|
28
|
+
|
|
29
|
+
`Group.create()` in `src/core/Group.ts` computes a dot-path using the current parent, registers the group in the registry, switches the current parent to the new group path, runs the callback, and then restores the previous parent. That means every route or API declared inside the callback receives a parent path that includes the group.
|
|
30
|
+
|
|
31
|
+
After the callback, chainable methods such as `withCache()` and `before()` iterate through `Registry.i.getChildren(Registry.i.getFullPath(this))` and copy settings to children that do not already define their own values.
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
graph TD
|
|
35
|
+
A[Group.create shop] --> B[register group]
|
|
36
|
+
B --> C[setCurrentParent shop]
|
|
37
|
+
C --> D[Api.create catalog]
|
|
38
|
+
C --> E[Group.create admin]
|
|
39
|
+
E --> F[Route.get list]
|
|
40
|
+
E --> G[Route.post create]
|
|
41
|
+
B --> H[withRetry or before propagates to children]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That propagation model explains an important behavior: group settings affect the elements created during the callback because the group methods run after registration is complete.
|
|
45
|
+
|
|
46
|
+
## Basic Usage
|
|
47
|
+
|
|
48
|
+
This example groups related routes under one API:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Api, Group, Klaim, Route } from "klaim";
|
|
52
|
+
|
|
53
|
+
Api.create("store", "https://dummyjson.com", () => {
|
|
54
|
+
Group.create("products", () => {
|
|
55
|
+
Route.get("list", "/products");
|
|
56
|
+
Route.get("getOne", "/products/[id]");
|
|
57
|
+
}).withCache(60);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const products = await Klaim.store.products.list();
|
|
61
|
+
const phone = await Klaim.store.products.getOne({ id: 1 });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Advanced Usage
|
|
65
|
+
|
|
66
|
+
This example groups multiple APIs at the root and gives them a shared timeout and route hook through the group.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { Api, Group, Hook, Klaim, Route } from "klaim";
|
|
70
|
+
|
|
71
|
+
Group.create("services", () => {
|
|
72
|
+
Api.create("auth", "https://example.com/auth", () => {
|
|
73
|
+
Route.post("login", "/login");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
Api.create("billing", "https://example.com/billing", () => {
|
|
77
|
+
Route.get("invoices", "/invoices");
|
|
78
|
+
});
|
|
79
|
+
})
|
|
80
|
+
.withTimeout(2, "Shared service timeout")
|
|
81
|
+
.onCall(() => {
|
|
82
|
+
console.log("A grouped service route is being called");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Hook.subscribe("auth.login", () => {
|
|
86
|
+
console.log("login completed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await Klaim.services.auth.login({}, { email: "a@example.com", password: "secret" });
|
|
90
|
+
await Klaim.services.billing.invoices();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Relationship to APIs and the Registry
|
|
94
|
+
|
|
95
|
+
Groups do not execute requests themselves. They exist to change the parent path that `Registry` uses and to copy configuration into children. APIs still provide the base URL. Routes still provide the HTTP method and path. The final call target always remains a route function attached somewhere under `Klaim`.
|
|
96
|
+
|
|
97
|
+
Because `Registry.getApi()` searches upward through the dot-path, nested routes can still find their owning API even when they live several group levels below it.
|
|
98
|
+
|
|
99
|
+
<Callout type="warn">`Group` inherits `withRate()` and `withPagination()` from `Element`, but `src/core/Group.ts` does not propagate those settings and `src/core/Klaim.ts` only reads rate limits from APIs and routes and pagination from routes. In other words, route groups are a good fit for cache, retry, timeout, and callbacks, but not for shared pagination or shared rate limiting in the current implementation.</Callout>
|
|
100
|
+
|
|
101
|
+
<Accordions>
|
|
102
|
+
<Accordion title="Why use groups instead of flat route names?">
|
|
103
|
+
Flat route names scale poorly once an API has more than a dozen operations because semantic boundaries disappear. A grouped tree such as `Klaim.store.products.list()` expresses intent more clearly than `Klaim.storeListProducts()`, and the registry already stores dot-paths internally, so the grouping model is cheap to maintain. The trade-off is that dynamic object traversal becomes deeper, which can make ad hoc introspection slightly more awkward. For most real integrations, the readability gain is worth that extra nesting.
|
|
104
|
+
</Accordion>
|
|
105
|
+
<Accordion title="What are the limits of group-level inheritance?">
|
|
106
|
+
Group inheritance is copy-based, not live composition. When you call `.withCache()` or `.before()` on a group, `src/core/Group.ts` walks the already-created children and fills in missing values, but it does not keep a stack of inherited middleware or recompute descendants later. That makes the implementation simple and avoids complicated merge logic, but it also means one child callback can replace another instead of forming a pipeline. If you need truly layered middleware semantics, you have to model them yourself inside one callback function.
|
|
107
|
+
</Accordion>
|
|
108
|
+
</Accordions>
|
|
109
|
+
|
|
110
|
+
The next useful page is [Request Lifecycle](/docs/request-lifecycle), which shows what happens after a grouped route is actually invoked.
|