restix 0.0.1 → 0.0.3

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 CHANGED
@@ -1,61 +1,68 @@
1
1
  # restix
2
2
 
3
3
  **restix** is a strict, type-safe HTTP client builder designed for **CLI tools and backend applications**.
4
- It lets you define your API **once** as a contract (routes + mappings) and then make requests without rewriting fetch logic, URL building, or manual payload handling.
5
4
 
6
- The goal is simple:
5
+ It allows you to define your API **once** as a contract and interact with it safely without rewriting fetch logic, URL builders, or request payload handling.
7
6
 
8
- > **Design your API → map it → call it safely**
7
+ > Design your API → map it → call it safely
9
8
 
10
- No runtime magic, no hidden abstractions, no guessing.
9
+ No runtime magic.
10
+ No hidden abstractions.
11
+ No guessing.
11
12
 
12
13
  ---
13
14
 
14
15
  ## Why restix?
15
16
 
16
17
  When building CLI tools or backend services, you often need to:
18
+
17
19
  - Call many HTTP endpoints
18
- - Inject params, query strings, headers, and body data
20
+ - Inject URL params, query strings, headers, and body data
19
21
  - Keep everything type-safe
20
- - Avoid rewriting request logic over and over
22
+ - Avoid request boilerplate
21
23
 
22
24
  **restix solves this by:**
25
+
23
26
  - Enforcing **exact input types** per endpoint
24
27
  - Preventing extra or missing fields at compile time
25
- - Automatically injecting params, query, body, and headers
26
- - Keeping request logic centralized and reusable
28
+ - Supporting multiple injection strategies
29
+ - Centralizing request logic
30
+ - Staying minimal and predictable
27
31
 
28
32
  ---
29
33
 
30
34
  ## Core Concepts
31
35
 
32
- ### 1. API Interface (Contract)
36
+ ### 1. API Contract (Type First)
33
37
 
34
- You define your API as a **TypeScript interface**.
35
- Each key represents a route, and its value represents the **exact data shape** allowed.
38
+ Your API is defined as a TypeScript interface.
39
+ Each key represents an endpoint, and its value represents the **exact allowed input**.
36
40
 
37
41
  ```ts
38
42
  interface API_INTERFACE {
39
- 'GET /users/{id}': {
40
- id: string
43
+ 'GET /packages/cli/name/{name}': {
44
+ name: string
41
45
  }
42
46
 
43
- 'POST /users/create': {
47
+ 'POST /packages/cli/create': {
44
48
  name: string
45
- email: string
49
+ version: string
50
+ developer: string
46
51
  }
47
52
  }
48
53
  ````
49
54
 
50
- This interface becomes the **single source of truth**.
55
+ This interface is the **single source of truth**.
51
56
 
52
57
  ---
53
58
 
54
59
  ### 2. API Mapping
55
60
 
56
- Each endpoint must be mapped to explain **where data goes**:
61
+ Each endpoint must declare how its data is injected.
62
+
63
+ Supported injection targets:
57
64
 
58
- * `params` → URL params
65
+ * `params` → URL parameters
59
66
  * `query` → query string
60
67
  * `body` → request body
61
68
  * `headers` → request headers
@@ -64,19 +71,15 @@ Each endpoint must be mapped to explain **where data goes**:
64
71
  import { apiMap, type API_MAPS, type GET_API } from 'restix'
65
72
 
66
73
  const api_maps: API_MAPS<API_INTERFACE> = {
67
- 'GET /users/{id}': {
68
- map: apiMap<GET_API<API_INTERFACE, 'GET /users/{id}'>>({
69
- params: ['id']
74
+ 'GET /packages/cli/name/{name}': {
75
+ map: apiMap<GET_API<API_INTERFACE, 'GET /packages/cli/name/{name}'>>({
76
+ params: ['name']
70
77
  }),
71
- res: 'json',
72
- status: {
73
- safe: 200,
74
- 404: () => 'User not found'
75
- }
78
+ res: 'json'
76
79
  },
77
80
 
78
- 'POST /users/create': {
79
- map: apiMap<GET_API<API_INTERFACE, 'POST /users/create'>>({
81
+ 'POST /packages/cli/create': {
82
+ map: apiMap<GET_API<API_INTERFACE, 'POST /packages/cli/create'>>({
80
83
  body: ['*']
81
84
  }),
82
85
  res: 'json',
@@ -87,75 +90,150 @@ const api_maps: API_MAPS<API_INTERFACE> = {
87
90
  }
88
91
  ```
89
92
 
90
- * `'*'` means **all fields**
91
- * Explicit keys mean **only those fields**
93
+ `'*'` means **all fields**
94
+ Explicit keys mean **only those fields**
95
+
96
+ ---
97
+
98
+ ### 3. Query Injection Strategies
99
+
100
+ #### A) Automatic Query Injection
101
+
102
+ ```ts
103
+ map: apiMap({
104
+ query: ['q', 'v']
105
+ })
106
+ ```
107
+
108
+ ```ts
109
+ api.request('GET /search', {
110
+ q: 'nex',
111
+ v: '1'
112
+ })
113
+ ```
114
+
115
+ ➡️ `?q=nex&v=1`
116
+
117
+ ---
118
+
119
+ #### B) URL-Positioned Query Injection
120
+
121
+ ```ts
122
+ 'GET /packages/cli/search?selam={v}&VERSION={v}'
123
+ ```
124
+
125
+ ```ts
126
+ api.request(
127
+ 'GET /packages/cli/search?selam={v}&VERSION={v}',
128
+ {
129
+ q: 'nex',
130
+ v: 'sa'
131
+ }
132
+ )
133
+ ```
134
+
135
+ ➡️ `?selam=sa&VERSION=sa`
136
+
137
+ This is especially useful for:
138
+
139
+ * Legacy APIs
140
+ * Fixed query naming conventions
141
+ * Enterprise-style endpoints
92
142
 
93
143
  ---
94
144
 
95
- ### 3. Creating the Client
145
+ ### 4. Creating the Client
96
146
 
97
147
  ```ts
98
148
  import { NexAPI } from 'restix'
99
149
 
100
150
  const api = new NexAPI<API_INTERFACE>(
101
- 'https://api.example.com',
151
+ 'http://localhost:5000',
102
152
  api_maps
103
153
  )
104
154
  ```
105
155
 
106
- You can also provide **default injected data** (headers, query, body, params):
156
+ #### With Defaults
107
157
 
108
158
  ```ts
109
159
  const api = new NexAPI<API_INTERFACE>(
110
- 'https://api.example.com',
160
+ 'http://localhost:5000',
111
161
  api_maps,
112
162
  {
113
163
  headers: {
114
- authorization: 'Bearer TOKEN'
164
+ 'User-Agent': 'restix-cli'
165
+ },
166
+ status: {
167
+ 404: () => 'Global not found'
115
168
  }
116
169
  }
117
170
  )
118
171
  ```
119
172
 
173
+ Defaults are **merged**, not replaced.
174
+
120
175
  ---
121
176
 
122
- ### 4. Making Requests (Type-Safe)
177
+ ### 5. Making Requests (Type-Safe)
123
178
 
124
179
  ```ts
125
- await api.request('GET /users/{id}', {
126
- id: '123'
180
+ await api.request('GET /packages/cli/name/{name}', {
181
+ name: 'xnex'
127
182
  })
128
183
  ```
129
184
 
130
185
  ```ts
131
- await api.request('POST /users/create', {
132
- name: 'John',
133
- email: 'john@example.com'
186
+ await api.request('POST /packages/cli/create', {
187
+ name: 'xnex',
188
+ version: '0.0.1',
189
+ developer: 'Signor P'
134
190
  })
135
191
  ```
136
192
 
137
- This will **fail at compile time**:
193
+ Compile-time error (extra field):
138
194
 
139
195
  ```ts
140
- api.request('POST /users/create', {
141
- name: 'John',
142
- email: 'john@example.com',
143
- extra: 'not allowed'
196
+ api.request('GET /packages/cli/name/{name}', {
197
+ name: 'xnex',
198
+ extra: 'nope'
144
199
  })
145
200
  ```
146
201
 
147
- Because **restix enforces exact types**.
202
+ Compile-time error (missing field):
203
+
204
+ ```ts
205
+ api.request('POST /packages/cli/create', {
206
+ name: 'xnex'
207
+ })
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Status Handling
213
+
214
+ Each endpoint can define status handlers:
215
+
216
+ ```ts
217
+ status: {
218
+ safe: 200,
219
+ 404: () => 'Not found',
220
+ 400: () => 'Bad request'
221
+ }
222
+ ```
223
+
224
+ You can also define **global status defaults** when creating the client.
148
225
 
149
226
  ---
150
227
 
151
228
  ## Key Features
152
229
 
153
- * ✅ Exact object validation (no extra keys)
230
+ * ✅ Exact object typing (no extra keys)
154
231
  * ✅ Route-based type inference
155
- * ✅ Automatic URL param injection
156
- * ✅ Automatic query string building
157
- * ✅ Automatic body & header extraction
158
- * ✅ CLI-friendly (works perfectly with Bun / Node)
232
+ * ✅ URL param injection
233
+ * ✅ Automatic & positional query injection
234
+ * ✅ Header & body extraction
235
+ * ✅ Global + local status handling
236
+ * ✅ CLI-friendly (Bun / Node)
159
237
  * ✅ No runtime dependencies
160
238
  * ✅ No decorators, no reflection
161
239
 
@@ -165,12 +243,12 @@ Because **restix enforces exact types**.
165
243
 
166
244
  * CLI tools
167
245
  * Backend services
168
- * Internal tooling
169
246
  * Automation scripts
170
- * API SDKs
171
- * Monorepo shared clients
247
+ * Internal SDKs
248
+ * API clients
249
+ * Monorepo shared tooling
172
250
 
173
- **Not** designed for browser-heavy frontend frameworks.
251
+ Not designed for heavy frontend frameworks.
174
252
 
175
253
  ---
176
254
 
@@ -181,7 +259,7 @@ restix follows a **contract-first** approach:
181
259
  * Types are the contract
182
260
  * Mapping defines behavior
183
261
  * Requests are deterministic
184
- * Errors are caught at compile time, not runtime
262
+ * Errors are caught at compile time
185
263
 
186
264
  If TypeScript allows it, restix allows it.
187
265
  If TypeScript rejects it, restix rejects it too.
@@ -190,10 +268,10 @@ If TypeScript rejects it, restix rejects it too.
190
268
 
191
269
  ## Status
192
270
 
193
- This package is **actively evolving**.
271
+ This project is actively evolving.
194
272
  The API is intentionally minimal and strict.
195
273
 
196
- Contributions, ideas, and feedback are welcome.
274
+ Ideas, issues, and contributions are welcome.
197
275
 
198
276
  ---
199
277
 
package/api.d.ts CHANGED
@@ -8,6 +8,10 @@ export declare class NexAPI<API_INTERFACE extends {
8
8
  params?: Record<string, string>;
9
9
  query?: Record<string, string>;
10
10
  headers?: Record<string, string>;
11
+ status?: {
12
+ safe?: number;
13
+ [n: number]: (res: Response) => any;
14
+ };
11
15
  });
12
16
  request<URL extends (keyof API_INTERFACE) & string>(prefix: URL, data: Exact<API_INTERFACE[URL], API_INTERFACE[URL]>): Promise<any>;
13
17
  }
package/api.js CHANGED
@@ -1,16 +1,42 @@
1
+ import { Status } from './status';
1
2
  export class NexAPI {
2
3
  #base = 'http://localhost:5000';
3
4
  #api_maps;
4
- #defaults;
5
+ #defaults = {
6
+ headers: { 'X-Client-Signature': 'restix/0.0.2 (by SignorP)' },
7
+ status: {
8
+ safe: Status.OK(undefined).status,
9
+ 201: Status.CREATED,
10
+ 202: Status.ACCEPTED,
11
+ 204: Status.NO_CONTENT,
12
+ 400: () => Status.BAD_REQUEST('Invalid request data', 'Check required parameters'),
13
+ 401: () => Status.UNAUTHORIZED('Authentication required'),
14
+ 403: () => Status.FORBIDDEN(),
15
+ 404: () => Status.NOT_FOUND(),
16
+ 405: () => Status.METHOD_NOT_ALLOWED(),
17
+ 408: () => Status.REQUEST_TIMEOUT(),
18
+ 409: () => Status.CONFLICT(),
19
+ 413: () => Status.PAYLOAD_TOO_LARGE(),
20
+ 415: () => Status.UNSUPPORTED_MEDIA_TYPE(),
21
+ 422: () => Status.UNPROCESSABLE_ENTITY('Validation failed'),
22
+ 429: () => Status.TOO_MANY_REQUESTS('Rate limit exceeded', 'Try again later'),
23
+ 500: () => Status.INTERNAL_ERROR(),
24
+ 501: () => Status.NOT_IMPLEMENTED(),
25
+ 502: () => Status.BAD_GATEWAY(),
26
+ 503: () => Status.SERVICE_UNAVAILABLE(),
27
+ 504: () => Status.GATEWAY_TIMEOUT()
28
+ }
29
+ };
5
30
  constructor(base, api_maps, defaults) {
6
31
  this.#base = base;
7
32
  this.#api_maps = api_maps;
8
- this.#defaults = defaults ?? {};
33
+ this.#defaults = { ...defaults, headers: { ...defaults?.headers ?? {}, ...this.#defaults.headers }, status: { ...this.#defaults.status, ...defaults?.status ?? {} } };
9
34
  }
10
35
  async request(prefix, data) {
11
36
  const api = this.#api_maps[prefix];
12
37
  if (!api)
13
38
  return;
39
+ api.status = { ...this.#defaults.status, ...(api.status ?? {}) };
14
40
  data = {
15
41
  ...data,
16
42
  ...this.#defaults.body ?? {},
@@ -32,16 +58,17 @@ export class NexAPI {
32
58
  url = this.#injectUrlData(url, api.map.params, data);
33
59
  if (api.map.query.length > 0)
34
60
  url = this.#injectQuery(url, api.map.query, data);
35
- if (api.map.body.length > 0)
61
+ if (api.map.body.length > 0) {
36
62
  fetchOptoin.body = JSON.stringify(this.#injectData(api.map.body, data));
63
+ fetchOptoin.headers = { 'Content-type': 'application/json' };
64
+ }
37
65
  if (api.map.headers.length > 0)
38
- fetchOptoin.headers = this.#injectData(api.map.headers, data);
39
- console.log(url, method, { ...fetchOptoin });
66
+ fetchOptoin.headers = { ...this.#injectData(api.map.headers, data), ...(fetchOptoin.headers ?? {}) };
40
67
  const res = await fetch(url, fetchOptoin);
41
68
  if (res.status === api.status.safe)
42
69
  return await res[api.res]();
43
70
  else if (api.status && api.status[res.status])
44
- return api.status[res.status]?.();
71
+ return await api.status[res.status]?.(res);
45
72
  return undefined;
46
73
  }
47
74
  #injectUrlData(url, params, data) {
@@ -50,13 +77,22 @@ export class NexAPI {
50
77
  return url;
51
78
  }
52
79
  #injectQuery(url, queries, data) {
53
- for (const [i, query] of queries.entries()) {
54
- if (i == 0)
55
- url = `${url}?${query}=${data[query]}`;
56
- else
57
- url = `${url}&${query}=${data[query]}`;
80
+ for (const query of queries[0] == '*' ? Object.keys(data) : queries) {
81
+ if (!data[query])
82
+ continue;
83
+ const key = `={${query}}`;
84
+ if (url.includes(key)) {
85
+ url = url.replaceAll(key, `=${data[query]}`);
86
+ continue;
87
+ }
88
+ if (!url.includes('?'))
89
+ url = `${url}?${query}=${data[query]}&`;
90
+ else if (!url.endsWith('&'))
91
+ url = `${url}&${query}=${data[query]}&`;
92
+ else if (url.endsWith('&'))
93
+ url = `${url}${query}=${data[query]}&`;
58
94
  }
59
- return url;
95
+ return url.endsWith('&') ? url.slice(0, url.length - 1) : url;
60
96
  }
61
97
  #injectData(body, data) {
62
98
  const res = {};
package/api.ts CHANGED
@@ -1,30 +1,73 @@
1
- import type { API_MAPS, Exact } from './types'
1
+ import { Status } from './status';
2
+ import type { API_MAPS, Exact } from './types';
2
3
 
3
4
  export class NexAPI<API_INTERFACE extends { [k: string]: any }> {
4
5
  #base = 'http://localhost:5000'
5
-
6
6
  #api_maps: API_MAPS<API_INTERFACE>
7
7
  #defaults: {
8
8
  body?: Record<string, string>
9
9
  params?: Record<string, string>
10
10
  query?: Record<string, string>
11
11
  headers?: Record<string, string>
12
- }
12
+ status: {
13
+ safe: number,
14
+ [n: number]: (res: Response) => any
15
+ }
16
+ } = {
17
+ headers: { 'X-Client-Signature': 'restix/0.0.2 (by SignorP)' },
18
+ status: {
19
+ safe: Status.OK(undefined).status,
20
+ 201: Status.CREATED,
21
+ 202: Status.ACCEPTED,
22
+ 204: Status.NO_CONTENT,
23
+ 400: () => Status.BAD_REQUEST(
24
+ 'Invalid request data',
25
+ 'Check required parameters'
26
+ ),
27
+ 401: () => Status.UNAUTHORIZED(
28
+ 'Authentication required'
29
+ ),
30
+ 403: () => Status.FORBIDDEN(),
31
+ 404: () => Status.NOT_FOUND(),
32
+ 405: () => Status.METHOD_NOT_ALLOWED(),
33
+ 408: () => Status.REQUEST_TIMEOUT(),
34
+ 409: () => Status.CONFLICT(),
35
+ 413: () => Status.PAYLOAD_TOO_LARGE(),
36
+ 415: () => Status.UNSUPPORTED_MEDIA_TYPE(),
37
+ 422: () => Status.UNPROCESSABLE_ENTITY(
38
+ 'Validation failed'
39
+ ),
40
+ 429: () => Status.TOO_MANY_REQUESTS(
41
+ 'Rate limit exceeded',
42
+ 'Try again later'
43
+ ),
44
+ 500: () => Status.INTERNAL_ERROR(),
45
+ 501: () => Status.NOT_IMPLEMENTED(),
46
+ 502: () => Status.BAD_GATEWAY(),
47
+ 503: () => Status.SERVICE_UNAVAILABLE(),
48
+ 504: () => Status.GATEWAY_TIMEOUT()
49
+ }
50
+ }
13
51
 
14
52
  constructor(base: string, api_maps: API_MAPS<API_INTERFACE>, defaults?: {
15
53
  body?: Record<string, string>
16
54
  params?: Record<string, string>
17
55
  query?: Record<string, string>
18
56
  headers?: Record<string, string>
57
+ status?: {
58
+ safe?: number,
59
+ [n: number]: (res: Response) => any
60
+ }
19
61
  }) {
20
62
  this.#base = base
21
63
  this.#api_maps = api_maps
22
- this.#defaults = defaults ?? {}
64
+ this.#defaults = { ...defaults, headers: { ...defaults?.headers ?? {}, ...this.#defaults.headers }, status: { ...this.#defaults.status, ...defaults?.status ?? {} } }
23
65
  }
24
66
 
25
67
  async request<URL extends (keyof API_INTERFACE) & string>(prefix: URL, data: Exact<API_INTERFACE[URL], API_INTERFACE[URL]>) {
26
68
  const api = this.#api_maps[prefix]
27
69
  if (!api) return
70
+ api.status = { ...this.#defaults.status, ...(api.status ?? {}) }
28
71
  data = {
29
72
  ...data,
30
73
  ...this.#defaults.body ?? {},
@@ -44,12 +87,14 @@ export class NexAPI<API_INTERFACE extends { [k: string]: any }> {
44
87
  url = `${this.#base}${url}`
45
88
  if (api.map.params.length > 0) url = this.#injectUrlData(url, api.map.params, data)
46
89
  if (api.map.query.length > 0) url = this.#injectQuery(url, api.map.query, data)
47
- if (api.map.body.length > 0) fetchOptoin.body = JSON.stringify(this.#injectData(api.map.body, data))
48
- if (api.map.headers.length > 0) fetchOptoin.headers = this.#injectData(api.map.headers, data)
49
- console.log(url, method, { ...fetchOptoin })
90
+ if (api.map.body.length > 0) {
91
+ fetchOptoin.body = JSON.stringify(this.#injectData(api.map.body, data))
92
+ fetchOptoin.headers = { 'Content-type': 'application/json' }
93
+ }
94
+ if (api.map.headers.length > 0) fetchOptoin.headers = { ...this.#injectData(api.map.headers, data), ...(fetchOptoin.headers ?? {}) }
50
95
  const res = await fetch(url, fetchOptoin)
51
96
  if (res.status === api.status.safe) return await res[api.res]()
52
- else if (api.status && api.status[res.status]) return api.status[res.status]?.()
97
+ else if (api.status && api.status[res.status]) return await api.status[res.status]?.(res)
53
98
  return undefined
54
99
  }
55
100
 
@@ -59,11 +104,18 @@ export class NexAPI<API_INTERFACE extends { [k: string]: any }> {
59
104
  }
60
105
 
61
106
  #injectQuery(url: string, queries: string[], data: Record<string, string>) {
62
- for (const [i, query] of queries.entries()) {
63
- if (i == 0) url = `${url}?${query}=${data[query]}`
64
- else url = `${url}&${query}=${data[query]}`
107
+ for (const query of queries[0] == '*' ? Object.keys(data) : queries) {
108
+ if (!data[query]) continue
109
+ const key = `={${query}}`
110
+ if (url.includes(key)) {
111
+ url = url.replaceAll(key, `=${data[query]}`)
112
+ continue
113
+ }
114
+ if (!url.includes('?')) url = `${url}?${query}=${data[query]}&`
115
+ else if (!url.endsWith('&')) url = `${url}&${query}=${data[query]}&`
116
+ else if (url.endsWith('&')) url = `${url}${query}=${data[query]}&`
65
117
  }
66
- return url
118
+ return url.endsWith('&') ? url.slice(0, url.length - 1) : url
67
119
  }
68
120
 
69
121
  #injectData(body: string[], data: Record<string, any>) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restix",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Type-safe HTTP client builder for CLI tools and backend applications",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",
package/status.d.ts ADDED
@@ -0,0 +1,128 @@
1
+ export declare const Status: {
2
+ readonly OK: <T>(data: T) => {
3
+ ok: boolean;
4
+ status: number;
5
+ data: T;
6
+ };
7
+ readonly CREATED: <T>(data: T) => {
8
+ ok: boolean;
9
+ status: number;
10
+ data: T;
11
+ };
12
+ readonly ACCEPTED: <T>(data: T) => {
13
+ ok: boolean;
14
+ status: number;
15
+ data: T;
16
+ };
17
+ readonly NO_CONTENT: () => {
18
+ ok: boolean;
19
+ status: number;
20
+ data: null;
21
+ };
22
+ readonly BAD_REQUEST: (message?: string, hint?: string) => {
23
+ ok: boolean;
24
+ status: number;
25
+ code: string;
26
+ message: string;
27
+ hint: string | undefined;
28
+ };
29
+ readonly UNAUTHORIZED: (message?: string, hint?: string) => {
30
+ ok: boolean;
31
+ status: number;
32
+ code: string;
33
+ message: string;
34
+ hint: string | undefined;
35
+ };
36
+ readonly FORBIDDEN: (message?: string) => {
37
+ ok: boolean;
38
+ status: number;
39
+ code: string;
40
+ message: string;
41
+ };
42
+ readonly NOT_FOUND: (message?: string) => {
43
+ ok: boolean;
44
+ status: number;
45
+ code: string;
46
+ message: string;
47
+ };
48
+ readonly METHOD_NOT_ALLOWED: (message?: string) => {
49
+ ok: boolean;
50
+ status: number;
51
+ code: string;
52
+ message: string;
53
+ };
54
+ readonly NOT_ACCEPTABLE: (message?: string) => {
55
+ ok: boolean;
56
+ status: number;
57
+ code: string;
58
+ message: string;
59
+ };
60
+ readonly REQUEST_TIMEOUT: (message?: string) => {
61
+ ok: boolean;
62
+ status: number;
63
+ code: string;
64
+ message: string;
65
+ };
66
+ readonly CONFLICT: (message?: string) => {
67
+ ok: boolean;
68
+ status: number;
69
+ code: string;
70
+ message: string;
71
+ };
72
+ readonly PAYLOAD_TOO_LARGE: (message?: string) => {
73
+ ok: boolean;
74
+ status: number;
75
+ code: string;
76
+ message: string;
77
+ };
78
+ readonly UNSUPPORTED_MEDIA_TYPE: (message?: string) => {
79
+ ok: boolean;
80
+ status: number;
81
+ code: string;
82
+ message: string;
83
+ };
84
+ readonly UNPROCESSABLE_ENTITY: (message?: string, hint?: string) => {
85
+ ok: boolean;
86
+ status: number;
87
+ code: string;
88
+ message: string;
89
+ hint: string | undefined;
90
+ };
91
+ readonly TOO_MANY_REQUESTS: (message?: string, hint?: string) => {
92
+ ok: boolean;
93
+ status: number;
94
+ code: string;
95
+ message: string;
96
+ hint: string | undefined;
97
+ };
98
+ readonly INTERNAL_ERROR: (message?: string) => {
99
+ ok: boolean;
100
+ status: number;
101
+ code: string;
102
+ message: string;
103
+ };
104
+ readonly NOT_IMPLEMENTED: (message?: string) => {
105
+ ok: boolean;
106
+ status: number;
107
+ code: string;
108
+ message: string;
109
+ };
110
+ readonly BAD_GATEWAY: (message?: string) => {
111
+ ok: boolean;
112
+ status: number;
113
+ code: string;
114
+ message: string;
115
+ };
116
+ readonly SERVICE_UNAVAILABLE: (message?: string) => {
117
+ ok: boolean;
118
+ status: number;
119
+ code: string;
120
+ message: string;
121
+ };
122
+ readonly GATEWAY_TIMEOUT: (message?: string) => {
123
+ ok: boolean;
124
+ status: number;
125
+ code: string;
126
+ message: string;
127
+ };
128
+ };
package/status.js ADDED
@@ -0,0 +1,131 @@
1
+ export const Status = {
2
+ // ✅ SUCCESS
3
+ OK: (data) => ({
4
+ ok: true,
5
+ status: 200,
6
+ data
7
+ }),
8
+ CREATED: (data) => ({
9
+ ok: true,
10
+ status: 201,
11
+ data
12
+ }),
13
+ ACCEPTED: (data) => ({
14
+ ok: true,
15
+ status: 202,
16
+ data
17
+ }),
18
+ NO_CONTENT: () => ({
19
+ ok: true,
20
+ status: 204,
21
+ data: null
22
+ }),
23
+ // ❌ CLIENT ERRORS
24
+ BAD_REQUEST: (message = 'Bad request', hint) => ({
25
+ ok: false,
26
+ status: 400,
27
+ code: 'BAD_REQUEST',
28
+ message,
29
+ hint
30
+ }),
31
+ UNAUTHORIZED: (message = 'Unauthorized', hint) => ({
32
+ ok: false,
33
+ status: 401,
34
+ code: 'UNAUTHORIZED',
35
+ message,
36
+ hint
37
+ }),
38
+ FORBIDDEN: (message = 'Forbidden') => ({
39
+ ok: false,
40
+ status: 403,
41
+ code: 'FORBIDDEN',
42
+ message
43
+ }),
44
+ NOT_FOUND: (message = 'Not found') => ({
45
+ ok: false,
46
+ status: 404,
47
+ code: 'NOT_FOUND',
48
+ message
49
+ }),
50
+ METHOD_NOT_ALLOWED: (message = 'Method not allowed') => ({
51
+ ok: false,
52
+ status: 405,
53
+ code: 'METHOD_NOT_ALLOWED',
54
+ message
55
+ }),
56
+ NOT_ACCEPTABLE: (message = 'Not acceptable') => ({
57
+ ok: false,
58
+ status: 406,
59
+ code: 'NOT_ACCEPTABLE',
60
+ message
61
+ }),
62
+ REQUEST_TIMEOUT: (message = 'Request timeout') => ({
63
+ ok: false,
64
+ status: 408,
65
+ code: 'REQUEST_TIMEOUT',
66
+ message
67
+ }),
68
+ CONFLICT: (message = 'Conflict') => ({
69
+ ok: false,
70
+ status: 409,
71
+ code: 'CONFLICT',
72
+ message
73
+ }),
74
+ PAYLOAD_TOO_LARGE: (message = 'Payload too large') => ({
75
+ ok: false,
76
+ status: 413,
77
+ code: 'PAYLOAD_TOO_LARGE',
78
+ message
79
+ }),
80
+ UNSUPPORTED_MEDIA_TYPE: (message = 'Unsupported media type') => ({
81
+ ok: false,
82
+ status: 415,
83
+ code: 'UNSUPPORTED_MEDIA_TYPE',
84
+ message
85
+ }),
86
+ UNPROCESSABLE_ENTITY: (message = 'Unprocessable entity', hint) => ({
87
+ ok: false,
88
+ status: 422,
89
+ code: 'UNPROCESSABLE_ENTITY',
90
+ message,
91
+ hint
92
+ }),
93
+ TOO_MANY_REQUESTS: (message = 'Too many requests', hint) => ({
94
+ ok: false,
95
+ status: 429,
96
+ code: 'TOO_MANY_REQUESTS',
97
+ message,
98
+ hint
99
+ }),
100
+ // 💥 SERVER ERRORS
101
+ INTERNAL_ERROR: (message = 'Internal server error') => ({
102
+ ok: false,
103
+ status: 500,
104
+ code: 'INTERNAL_ERROR',
105
+ message
106
+ }),
107
+ NOT_IMPLEMENTED: (message = 'Not implemented') => ({
108
+ ok: false,
109
+ status: 501,
110
+ code: 'NOT_IMPLEMENTED',
111
+ message
112
+ }),
113
+ BAD_GATEWAY: (message = 'Bad gateway') => ({
114
+ ok: false,
115
+ status: 502,
116
+ code: 'BAD_GATEWAY',
117
+ message
118
+ }),
119
+ SERVICE_UNAVAILABLE: (message = 'Service unavailable') => ({
120
+ ok: false,
121
+ status: 503,
122
+ code: 'SERVICE_UNAVAILABLE',
123
+ message
124
+ }),
125
+ GATEWAY_TIMEOUT: (message = 'Gateway timeout') => ({
126
+ ok: false,
127
+ status: 504,
128
+ code: 'GATEWAY_TIMEOUT',
129
+ message
130
+ })
131
+ };
package/status.ts ADDED
@@ -0,0 +1,151 @@
1
+ export const Status = {
2
+ // ✅ SUCCESS
3
+ OK: <T>(data: T) => ({
4
+ ok: true,
5
+ status: 200,
6
+ data
7
+ }),
8
+
9
+ CREATED: <T>(data: T) => ({
10
+ ok: true,
11
+ status: 201,
12
+ data
13
+ }),
14
+
15
+ ACCEPTED: <T>(data: T) => ({
16
+ ok: true,
17
+ status: 202,
18
+ data
19
+ }),
20
+
21
+ NO_CONTENT: () => ({
22
+ ok: true,
23
+ status: 204,
24
+ data: null
25
+ }),
26
+
27
+ // ❌ CLIENT ERRORS
28
+ BAD_REQUEST: (message = 'Bad request', hint?: string) => ({
29
+ ok: false,
30
+ status: 400,
31
+ code: 'BAD_REQUEST',
32
+ message,
33
+ hint
34
+ }),
35
+
36
+ UNAUTHORIZED: (message = 'Unauthorized', hint?: string) => ({
37
+ ok: false,
38
+ status: 401,
39
+ code: 'UNAUTHORIZED',
40
+ message,
41
+ hint
42
+ }),
43
+
44
+ FORBIDDEN: (message = 'Forbidden') => ({
45
+ ok: false,
46
+ status: 403,
47
+ code: 'FORBIDDEN',
48
+ message
49
+ }),
50
+
51
+ NOT_FOUND: (message = 'Not found') => ({
52
+ ok: false,
53
+ status: 404,
54
+ code: 'NOT_FOUND',
55
+ message
56
+ }),
57
+
58
+ METHOD_NOT_ALLOWED: (message = 'Method not allowed') => ({
59
+ ok: false,
60
+ status: 405,
61
+ code: 'METHOD_NOT_ALLOWED',
62
+ message
63
+ }),
64
+
65
+ NOT_ACCEPTABLE: (message = 'Not acceptable') => ({
66
+ ok: false,
67
+ status: 406,
68
+ code: 'NOT_ACCEPTABLE',
69
+ message
70
+ }),
71
+
72
+ REQUEST_TIMEOUT: (message = 'Request timeout') => ({
73
+ ok: false,
74
+ status: 408,
75
+ code: 'REQUEST_TIMEOUT',
76
+ message
77
+ }),
78
+
79
+ CONFLICT: (message = 'Conflict') => ({
80
+ ok: false,
81
+ status: 409,
82
+ code: 'CONFLICT',
83
+ message
84
+ }),
85
+
86
+ PAYLOAD_TOO_LARGE: (message = 'Payload too large') => ({
87
+ ok: false,
88
+ status: 413,
89
+ code: 'PAYLOAD_TOO_LARGE',
90
+ message
91
+ }),
92
+
93
+ UNSUPPORTED_MEDIA_TYPE: (message = 'Unsupported media type') => ({
94
+ ok: false,
95
+ status: 415,
96
+ code: 'UNSUPPORTED_MEDIA_TYPE',
97
+ message
98
+ }),
99
+
100
+ UNPROCESSABLE_ENTITY: (message = 'Unprocessable entity', hint?: string) => ({
101
+ ok: false,
102
+ status: 422,
103
+ code: 'UNPROCESSABLE_ENTITY',
104
+ message,
105
+ hint
106
+ }),
107
+
108
+ TOO_MANY_REQUESTS: (message = 'Too many requests', hint?: string) => ({
109
+ ok: false,
110
+ status: 429,
111
+ code: 'TOO_MANY_REQUESTS',
112
+ message,
113
+ hint
114
+ }),
115
+
116
+ // 💥 SERVER ERRORS
117
+ INTERNAL_ERROR: (message = 'Internal server error') => ({
118
+ ok: false,
119
+ status: 500,
120
+ code: 'INTERNAL_ERROR',
121
+ message
122
+ }),
123
+
124
+ NOT_IMPLEMENTED: (message = 'Not implemented') => ({
125
+ ok: false,
126
+ status: 501,
127
+ code: 'NOT_IMPLEMENTED',
128
+ message
129
+ }),
130
+
131
+ BAD_GATEWAY: (message = 'Bad gateway') => ({
132
+ ok: false,
133
+ status: 502,
134
+ code: 'BAD_GATEWAY',
135
+ message
136
+ }),
137
+
138
+ SERVICE_UNAVAILABLE: (message = 'Service unavailable') => ({
139
+ ok: false,
140
+ status: 503,
141
+ code: 'SERVICE_UNAVAILABLE',
142
+ message
143
+ }),
144
+
145
+ GATEWAY_TIMEOUT: (message = 'Gateway timeout') => ({
146
+ ok: false,
147
+ status: 504,
148
+ code: 'GATEWAY_TIMEOUT',
149
+ message
150
+ })
151
+ } as const
package/test.test.js CHANGED
@@ -1,13 +1,109 @@
1
1
  import { NexAPI } from './api';
2
2
  import { apiMap } from './utils';
3
+ /* -------------------------------------------------------
4
+ API MAPS
5
+ ------------------------------------------------------- */
3
6
  const api_maps = {
4
- 'GET /users': {
5
- map: apiMap(),
7
+ // PARAM INJECTION
8
+ 'GET /packages/cli/name/{name}': {
9
+ map: apiMap({
10
+ params: ['name']
11
+ }),
12
+ res: 'json'
13
+ },
14
+ // MULTIPLE PARAMS
15
+ 'GET /packages/cli/name/{name}/{version}': {
16
+ map: apiMap({
17
+ params: ['*']
18
+ }),
6
19
  res: 'json',
7
20
  status: {
8
- safe: 200
21
+ 404: () => 'VERSION NOT FOUND'
9
22
  }
23
+ },
24
+ // BODY INJECTION
25
+ 'POST /packages/cli/create': {
26
+ map: apiMap({
27
+ body: ['*']
28
+ }),
29
+ res: 'json',
30
+ status: {
31
+ safe: 201,
32
+ 400: () => 'BAD REQUEST'
33
+ }
34
+ },
35
+ // QUERY + HEADER INJECTION
36
+ 'GET /packages/cli/search?selam={v}&VERSION={v}': {
37
+ map: apiMap({
38
+ query: ['q', 'v'],
39
+ headers: ['Authorization']
40
+ }),
41
+ res: 'json'
10
42
  }
11
43
  };
12
- const api = new NexAPI('http://localhost:5000', api_maps);
13
- console.log(JSON.stringify(await api.request('GET /users', { selam: 'selam' }), null, 4));
44
+ /* -------------------------------------------------------
45
+ API INSTANCE
46
+ ------------------------------------------------------- */
47
+ const api = new NexAPI('http://localhost:5000', api_maps, {
48
+ headers: {
49
+ 'User-Agent': 'restix-cli'
50
+ },
51
+ status: {
52
+ 404: () => '404 not found for request.'
53
+ }
54
+ });
55
+ /* -------------------------------------------------------
56
+ FEATURE SHOWCASE
57
+ ------------------------------------------------------- */
58
+ // -----------------------------------------
59
+ // SIMPLE PARAM REQUEST
60
+ // GET /packages/cli/name/xnex
61
+ // -----------------------------------------
62
+ console.log(await api.request('GET /packages/cli/name/{name}', {
63
+ name: 'xnex'
64
+ }));
65
+ // -----------------------------------------
66
+ // MULTI PARAM REQUEST
67
+ // GET /packages/cli/name/xnex/0.0.1
68
+ // -----------------------------------------
69
+ await api.request('GET /packages/cli/name/{name}/{version}', {
70
+ name: 'xnex',
71
+ version: '0.0.1'
72
+ });
73
+ // -----------------------------------------
74
+ // BODY PAYLOAD
75
+ // POST /packages/cli/create
76
+ // -----------------------------------------
77
+ await api.request('POST /packages/cli/create', {
78
+ name: 'xnex',
79
+ version: '0.0.1',
80
+ developer: 'Signor P'
81
+ });
82
+ // -----------------------------------------
83
+ // QUERY + HEADER
84
+ // GET /packages/cli/search?query=nex
85
+ // Header: token
86
+ // -----------------------------------------
87
+ await api.request('GET /packages/cli/search?selam={v}&VERSION={v}', {
88
+ q: 'nex',
89
+ v: 'sa',
90
+ Authorization: 'Bearer AUTH_TOKEN'
91
+ });
92
+ // -----------------------------------------
93
+ // TYPE SAFETY DEMO (INTENTIONAL ERRORS)
94
+ // -----------------------------------------
95
+ // ❌ extra field not allowed
96
+ // await api.request(
97
+ // 'GET /packages/cli/name/{name}',
98
+ // {
99
+ // name: 'xnex',
100
+ // extra: 'nope'
101
+ // }
102
+ // )
103
+ // ❌ missing field
104
+ // await api.request(
105
+ // 'POST /packages/cli/create',
106
+ // {
107
+ // name: 'xnex'
108
+ // }
109
+ // )
package/test.test.ts CHANGED
@@ -1,19 +1,174 @@
1
1
  import { NexAPI } from './api'
2
- import type { API_MAPS } from './types'
2
+ import type { API_MAPS, GET_API } from './types'
3
3
  import { apiMap } from './utils'
4
4
 
5
+ /* -------------------------------------------------------
6
+ API CONTRACT
7
+ ------------------------------------------------------- */
8
+
5
9
  interface API_INTERFACE {
6
- 'GET /users': {}
10
+ 'GET /packages/cli/name/{name}': {
11
+ name: string
12
+ }
13
+
14
+ 'GET /packages/cli/name/{name}/{version}': {
15
+ name: string
16
+ version: string
17
+ }
18
+
19
+ 'POST /packages/cli/create': {
20
+ name: string
21
+ version: string
22
+ developer: string
23
+ }
24
+
25
+ 'GET /packages/cli/search?selam={v}&VERSION={v}': {
26
+ q: string
27
+ v?: string,
28
+ Authorization: string
29
+ }
7
30
  }
31
+
32
+ /* -------------------------------------------------------
33
+ API MAPS
34
+ ------------------------------------------------------- */
35
+
8
36
  const api_maps: API_MAPS<API_INTERFACE> = {
9
- 'GET /users': {
10
- map: apiMap(),
37
+
38
+ // PARAM INJECTION
39
+ 'GET /packages/cli/name/{name}': {
40
+ map: apiMap<GET_API<API_INTERFACE, 'GET /packages/cli/name/{name}'>>({
41
+ params: ['name']
42
+ }),
43
+ res: 'json'
44
+ },
45
+
46
+ // MULTIPLE PARAMS
47
+ 'GET /packages/cli/name/{name}/{version}': {
48
+ map: apiMap<GET_API<API_INTERFACE, 'GET /packages/cli/name/{name}/{version}'>>({
49
+ params: ['*']
50
+ }),
11
51
  res: 'json',
12
52
  status: {
13
- safe: 200
53
+ 404: () => 'VERSION NOT FOUND'
14
54
  }
55
+ },
56
+
57
+ // BODY INJECTION
58
+ 'POST /packages/cli/create': {
59
+ map: apiMap<GET_API<API_INTERFACE, 'POST /packages/cli/create'>>({
60
+ body: ['*']
61
+ }),
62
+ res: 'json',
63
+ status: {
64
+ safe: 201,
65
+ 400: () => 'BAD REQUEST'
66
+ }
67
+ },
68
+
69
+ // QUERY + HEADER INJECTION
70
+ 'GET /packages/cli/search?selam={v}&VERSION={v}': {
71
+ map: apiMap<GET_API<API_INTERFACE, 'GET /packages/cli/search?selam={v}&VERSION={v}'>>({
72
+ query: ['q', 'v'],
73
+ headers: ['Authorization']
74
+ }),
75
+ res: 'json'
15
76
  }
16
77
  }
17
78
 
18
- const api = new NexAPI<API_INTERFACE>('http://localhost:5000', api_maps)
19
- console.log(JSON.stringify(await api.request('GET /users', { selam: 'selam' }), null, 4))
79
+ /* -------------------------------------------------------
80
+ API INSTANCE
81
+ ------------------------------------------------------- */
82
+
83
+ const api = new NexAPI<API_INTERFACE>(
84
+ 'http://localhost:5000',
85
+ api_maps,
86
+ {
87
+ headers: {
88
+ 'User-Agent': 'restix-cli'
89
+ },
90
+ status: {
91
+ 404: () => '404 not found for request.'
92
+ }
93
+ }
94
+ )
95
+
96
+ /* -------------------------------------------------------
97
+ FEATURE SHOWCASE
98
+ ------------------------------------------------------- */
99
+
100
+
101
+ // -----------------------------------------
102
+ // SIMPLE PARAM REQUEST
103
+ // GET /packages/cli/name/xnex
104
+ // -----------------------------------------
105
+ console.log(await api.request(
106
+ 'GET /packages/cli/name/{name}',
107
+ {
108
+ name: 'xnex'
109
+ }
110
+ ))
111
+
112
+
113
+ // -----------------------------------------
114
+ // MULTI PARAM REQUEST
115
+ // GET /packages/cli/name/xnex/0.0.1
116
+ // -----------------------------------------
117
+ await api.request(
118
+ 'GET /packages/cli/name/{name}/{version}',
119
+ {
120
+ name: 'xnex',
121
+ version: '0.0.1'
122
+ }
123
+ )
124
+
125
+
126
+ // -----------------------------------------
127
+ // BODY PAYLOAD
128
+ // POST /packages/cli/create
129
+ // -----------------------------------------
130
+ await api.request(
131
+ 'POST /packages/cli/create',
132
+ {
133
+ name: 'xnex',
134
+ version: '0.0.1',
135
+ developer: 'Signor P'
136
+ }
137
+ )
138
+
139
+
140
+ // -----------------------------------------
141
+ // QUERY + HEADER
142
+ // GET /packages/cli/search?query=nex
143
+ // Header: token
144
+ // -----------------------------------------
145
+ await api.request(
146
+ 'GET /packages/cli/search?selam={v}&VERSION={v}',
147
+ {
148
+ q: 'nex',
149
+ v: 'sa',
150
+ Authorization: 'Bearer AUTH_TOKEN'
151
+ }
152
+ )
153
+
154
+
155
+ // -----------------------------------------
156
+ // TYPE SAFETY DEMO (INTENTIONAL ERRORS)
157
+ // -----------------------------------------
158
+
159
+ // ❌ extra field not allowed
160
+ // await api.request(
161
+ // 'GET /packages/cli/name/{name}',
162
+ // {
163
+ // name: 'xnex',
164
+ // extra: 'nope'
165
+ // }
166
+ // )
167
+
168
+ // ❌ missing field
169
+ // await api.request(
170
+ // 'POST /packages/cli/create',
171
+ // {
172
+ // name: 'xnex'
173
+ // }
174
+ // )
package/types.d.ts CHANGED
@@ -8,9 +8,9 @@ export type Exact<T, Shape extends T> = T & Record<Exclude<keyof Shape, keyof T>
8
8
  export interface API_MAP_TYPE<K extends string> {
9
9
  map: ApiMapShape<K>;
10
10
  res: keyof Pick<Response, 'json' | 'text' | 'blob'>;
11
- status: {
12
- safe: number;
13
- [k: number]: () => any;
11
+ status?: {
12
+ safe?: number;
13
+ [n: number]: (res: Response) => any;
14
14
  };
15
15
  }
16
16
  export type API_MAPS<T extends Record<string, any>> = {
package/types.ts CHANGED
@@ -11,9 +11,9 @@ export type Exact<T, Shape extends T> =
11
11
  export interface API_MAP_TYPE<K extends string> {
12
12
  map: ApiMapShape<K>
13
13
  res: keyof Pick<Response, 'json' | 'text' | 'blob'>
14
- status: {
15
- safe: number
16
- [k: number]: () => any
14
+ status?: {
15
+ safe?: number,
16
+ [n: number]: (res: Response) => any
17
17
  }
18
18
  }
19
19