@dsanchos/api 1.0.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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@dsanchos/api",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "test": "echo \"Error: no test specified\" && exit 1",
15
+ "build": "tsc",
16
+ "prepublish": "npm run build"
17
+ },
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "type": "module",
22
+ "devDependencies": {
23
+ "@types/node": "^25.3.0",
24
+ "eslint": "^10.0.0",
25
+ "prettier": "^3.8.1",
26
+ "ts-node": "^10.9.2",
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^4.0.18"
29
+ },
30
+ "dependencies": {
31
+ "node-fetch": "^3.3.2"
32
+ }
33
+ }
package/readme.md ADDED
@@ -0,0 +1,211 @@
1
+ # api
2
+
3
+ A lightweight TypeScript fetch wrapper with caching, polling, interceptors, and logging.
4
+
5
+ Лёгкая TypeScript обёртка над fetch с кэшированием, поллингом, интерсепторами и логированием.
6
+
7
+ ---
8
+
9
+ ## Installation / Установка
10
+
11
+ ```bash
12
+ npm install node-fetch
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Quick Start / Быстрый старт
18
+
19
+ ```typescript
20
+ import { ApiCore, defineApi } from "./core.js";
21
+ import { api } from "./api.js";
22
+
23
+ // Configure once / Настройте один раз
24
+ ApiCore({
25
+ baseUrl: "https://jsonplaceholder.typicode.com",
26
+ baseKeepUnusedDataFor: 60,
27
+ baseHeader: { "Content-Type": "application/json" },
28
+ });
29
+
30
+ // Make a request / Сделайте запрос
31
+ const { data, isLoading, isError, error } = await api({
32
+ method: "GET",
33
+ url: "/posts",
34
+ });
35
+ ```
36
+
37
+ ---
38
+
39
+ ## ApiCore
40
+
41
+ Global configuration for all requests.
42
+
43
+ Глобальная конфигурация для всех запросов.
44
+
45
+ ```typescript
46
+ ApiCore({
47
+ baseUrl: "https://api.example.com", // Base URL for all requests
48
+ baseKeepUnusedDataFor: 60, // Cache TTL in seconds (default: 60)
49
+ baseHeader: {
50
+ // Default headers for all requests
51
+ "Content-Type": "application/json",
52
+ },
53
+ logging: true, // Enable request logging
54
+ interceptors: {
55
+ request: (props) => ({
56
+ // Modify request before sending
57
+ ...props,
58
+ headers: { Authorization: `Bearer ${getToken()}` },
59
+ }),
60
+ },
61
+ });
62
+ ```
63
+
64
+ ---
65
+
66
+ ## api()
67
+
68
+ ```typescript
69
+ const { data, isLoading, isFetching, isError, error } = await api({
70
+ method: "GET", // GET | POST | PUT | PATCH | DELETE
71
+ url: "/posts", // Path (baseUrl is prepended automatically)
72
+ headers: {}, // Override default headers
73
+ body: {}, // Request body (auto JSON.stringify)
74
+
75
+ // Cache
76
+ provideCache: "posts", // Save response under this key
77
+ keepUnusedDataFor: 30, // Override TTL for this request (seconds)
78
+ invalidateCache: "posts", // Delete this cache key after request
79
+ });
80
+ ```
81
+
82
+ ### Response / Ответ
83
+
84
+ | Field | Type | Description |
85
+ | ------------ | ---------------- | ---------------------- |
86
+ | `data` | `any` | Response data |
87
+ | `isLoading` | `boolean` | True on first load |
88
+ | `isFetching` | `boolean` | True on any request |
89
+ | `isError` | `boolean` | True if request failed |
90
+ | `error` | `string \| null` | Error message |
91
+
92
+ ---
93
+
94
+ ## Caching / Кэширование
95
+
96
+ ```typescript
97
+ // Save to cache / Сохранить в кэш
98
+ await api({
99
+ method: "GET",
100
+ url: "/posts",
101
+ provideCache: "posts", // Cache key
102
+ keepUnusedDataFor: 120, // 2 minutes
103
+ });
104
+
105
+ // Invalidate cache after mutation / Инвалидировать после мутации
106
+ await api({
107
+ method: "POST",
108
+ url: "/posts",
109
+ body: { title: "New post" },
110
+ invalidateCache: "posts", // Delete "posts" cache
111
+ });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Polling
117
+
118
+ ```typescript
119
+ import { polling } from "./polling.js";
120
+
121
+ const stop = polling(
122
+ {
123
+ method: "GET",
124
+ url: "/notifications",
125
+ pollingInterval: 3000, // Every 3 seconds / Каждые 3 секунды
126
+ stopAfter: 30000, // Stop after 30 seconds / Остановить через 30 секунд
127
+ },
128
+ (res, stop) => {
129
+ console.log(res.data);
130
+ if (res.isError) stop(); // Stop on error / Остановить при ошибке
131
+ },
132
+ );
133
+
134
+ // Stop manually / Остановить вручную
135
+ stop();
136
+ ```
137
+
138
+ ---
139
+
140
+ ## defineApi
141
+
142
+ Group related endpoints without writing types manually.
143
+
144
+ Группируйте связанные запросы без ручного написания типов.
145
+
146
+ ```typescript
147
+ const usersApi = defineApi({
148
+ getUsers: { method: "GET", url: "/users" },
149
+ createUser: { method: "POST", url: "/users" },
150
+ updateUser: { method: "PUT", url: "/users/1" },
151
+ deleteUser: { method: "DELETE", url: "/users/1" },
152
+ });
153
+
154
+ const { data } = await api(usersApi.getUsers);
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Logging / Логирование
160
+
161
+ When `logging: true` is set in `ApiCore`, every request is logged:
162
+
163
+ При `logging: true` в `ApiCore` каждый запрос логируется:
164
+
165
+ ```
166
+ ✅ [GET] /posts → 200 (123ms)
167
+ ✅ [POST] /posts → 201 (89ms)
168
+ ❌ [GET] /posts/99 → 404 (45ms)
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Interceptors
174
+
175
+ Modify every request automatically (e.g. add auth token).
176
+
177
+ Изменяйте каждый запрос автоматически (например, добавить токен).
178
+
179
+ ```typescript
180
+ ApiCore({
181
+ baseUrl: "https://api.example.com",
182
+ interceptors: {
183
+ request: (props) => ({
184
+ ...props,
185
+ headers: {
186
+ ...props.headers,
187
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
188
+ },
189
+ }),
190
+ },
191
+ });
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Project Structure / Структура проекта
197
+
198
+ ```
199
+ source/
200
+ ├── api.ts # Main fetch wrapper
201
+ ├── core.ts # ApiCore config + defineApi
202
+ ├── polling.ts # Polling utility
203
+ ├── logger.ts # Request logger
204
+ └── api.types.ts # TypeScript types
205
+ ```
206
+
207
+ ---
208
+
209
+ ## License / Лицензия
210
+
211
+ MIT
package/source/api.ts ADDED
@@ -0,0 +1,101 @@
1
+ import fetch from "node-fetch";
2
+ import type { API, APIResponse, CacheEntry } from "./api.types.js";
3
+ import { getConfig } from "./core.js";
4
+ import { Logger } from "./logger.js";
5
+
6
+ const cache = new Map<string, CacheEntry>();
7
+
8
+ export async function api(props: API): Promise<APIResponse> {
9
+ const {
10
+ baseUrl,
11
+ baseKeepUnusedDataFor = 60,
12
+ baseHeader,
13
+ interceptors,
14
+ logger,
15
+ } = getConfig();
16
+
17
+ const finalProps = interceptors?.request
18
+ ? await Promise.resolve(interceptors.request(props))
19
+ : props;
20
+
21
+ const {
22
+ method,
23
+ url,
24
+ headers,
25
+ body,
26
+ keepUnusedDataFor = baseKeepUnusedDataFor,
27
+ provideCache = "",
28
+ invalidateCache = "",
29
+ } = finalProps;
30
+
31
+ let data = null;
32
+ let isLoading = true;
33
+ let isFetching = true;
34
+ let isError = false;
35
+ let error = null;
36
+
37
+ const TTL = keepUnusedDataFor * 1000;
38
+ const now = Date.now();
39
+
40
+ if (provideCache) {
41
+ const cached = cache.get(provideCache);
42
+
43
+ if (cached && now - cached.timestamp < TTL) {
44
+ return {
45
+ data: cached.data,
46
+ isLoading: false,
47
+ isFetching: false,
48
+ isError: false,
49
+ error: null,
50
+ };
51
+ }
52
+ }
53
+
54
+ try {
55
+ isFetching = true;
56
+ if (!data) isLoading = true;
57
+
58
+ const start = performance.now();
59
+ const response = await fetch(`${baseUrl}${url}`, {
60
+ method,
61
+ headers: headers ?? baseHeader,
62
+ body: body ? JSON.stringify(body) : null,
63
+ });
64
+ const ms = Math.round(performance.now() - start);
65
+
66
+ if (logger) {
67
+ Logger(method, url, response.status, ms);
68
+ }
69
+
70
+ if (!response.ok) {
71
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
72
+ }
73
+
74
+ data = await response.json();
75
+
76
+ isLoading = false;
77
+
78
+ if (invalidateCache) {
79
+ for (const key of invalidateCache) {
80
+ cache.delete(key);
81
+ }
82
+ } else {
83
+ if (provideCache) {
84
+ cache.set(provideCache, { data, timestamp: now });
85
+ }
86
+ }
87
+ } catch (err) {
88
+ error = err instanceof Error ? err.message : String(err);
89
+ isError = true;
90
+ } finally {
91
+ isFetching = false;
92
+ }
93
+
94
+ return {
95
+ data,
96
+ isLoading,
97
+ isFetching,
98
+ isError,
99
+ error,
100
+ };
101
+ }
@@ -0,0 +1,60 @@
1
+ export type METHODS = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+
3
+ export type CacheEntry = {
4
+ data: any;
5
+ timestamp: number;
6
+ };
7
+
8
+ export type headersMap = {
9
+ "Content-Type":
10
+ | "application/json"
11
+ | "application/x-www-form-urlencoded"
12
+ | "multipart/form-data"
13
+ | "text/html"
14
+ | "text/plain";
15
+
16
+ Accept: "application/json" | "text/html" | "*/*";
17
+
18
+ Authorization: `Bearer ${string}` | `Basic ${string}` | `ApiKey ${string}`;
19
+
20
+ "Cache-Control": "no-cache" | "no-store" | "max-age=3600";
21
+
22
+ "Accept-Encoding": "gzip" | "deflate" | "gzip, deflate";
23
+ };
24
+
25
+ export type Headers = {
26
+ [K in keyof headersMap]?: headersMap[K];
27
+ };
28
+
29
+ export interface API {
30
+ method: METHODS;
31
+ url: string;
32
+ headers?: Headers;
33
+ body?: any;
34
+ keepUnusedDataFor?: number;
35
+ pollingInterval?: number;
36
+ stopAfter?: number;
37
+ provideCache?: string;
38
+ invalidateCache?: string[];
39
+ }
40
+
41
+ export interface APIResponse {
42
+ data: any;
43
+ isLoading: boolean;
44
+ isFetching: boolean;
45
+ isError: boolean;
46
+ error: string | null;
47
+ }
48
+
49
+ export type OnData = (res: any, stop: () => void) => void;
50
+
51
+ export interface CORE {
52
+ baseUrl: string;
53
+ baseKeepUnusedDataFor: number;
54
+ baseHeader: Headers;
55
+ interceptors?: {
56
+ request?: (props: API) => API | Promise<API>;
57
+ response?: (res: APIResponse) => APIResponse | Promise<APIResponse>;
58
+ };
59
+ logger?: boolean;
60
+ }
package/source/core.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { API, CORE } from "./api.types.js";
2
+
3
+ const config: CORE = {
4
+ baseUrl: "",
5
+ baseKeepUnusedDataFor: 60,
6
+ baseHeader: {},
7
+ interceptors: {},
8
+ logger: true,
9
+ };
10
+
11
+ export function ApiCore(props: CORE) {
12
+ config.baseUrl = props.baseUrl;
13
+ config.baseKeepUnusedDataFor = props.baseKeepUnusedDataFor;
14
+
15
+ config.baseHeader = props.baseHeader;
16
+
17
+ if (props.logger) {
18
+ config.logger = props.logger;
19
+ }
20
+
21
+ if (props.interceptors) {
22
+ config.interceptors = props.interceptors;
23
+ }
24
+ }
25
+
26
+ export function getConfig() {
27
+ return config;
28
+ }
29
+
30
+ export function defineApi<T extends Record<string, API>>(props: T) {
31
+ return props;
32
+ }
33
+
34
+ // Планы на будущее!
35
+ // retry.ts — повтор запроса при ошибке (retryCount, retryDelay)
36
+ // queue.ts — очередь запросов (не слать 100 запросов одновременно)
37
+ // abort.ts — отмена запроса через AbortController
@@ -0,0 +1,3 @@
1
+ export { api } from "./api.js";
2
+ export { ApiCore, defineApi } from "./core.js";
3
+ export { polling } from "./polling.js";
@@ -0,0 +1,9 @@
1
+ export function Logger(
2
+ method: string,
3
+ url: string,
4
+ status: number,
5
+ ms: number,
6
+ ) {
7
+ const isSuccess = status >= 400 ? "❌" : "✅";
8
+ console.log(`${isSuccess} [${method}] ${url} -> ${status} (${ms}ms)`);
9
+ }
@@ -0,0 +1,22 @@
1
+ import { api } from "./api.js";
2
+ import type { API, OnData } from "./api.types.js";
3
+
4
+ export function polling(props: API, onData: OnData): void {
5
+ const { pollingInterval = 3000, stopAfter } = props;
6
+ let isActive = true;
7
+
8
+ const stop = () => {
9
+ isActive = false;
10
+ };
11
+
12
+ async function poll() {
13
+ if (!isActive) return;
14
+ const res = await api(props);
15
+ onData(res, stop);
16
+ if (isActive) setTimeout(poll, pollingInterval);
17
+ }
18
+
19
+ poll();
20
+
21
+ if (stopAfter) setTimeout(stop, stopAfter);
22
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./source",
4
+ "outDir": "./dist",
5
+
6
+ "module": "nodenext",
7
+ "moduleResolution": "nodenext",
8
+ "target": "esnext",
9
+ "types": ["node"],
10
+
11
+ "sourceMap": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ // "noEmit": true,
15
+ // "allowImportingTsExtensions":
16
+
17
+ "noUncheckedIndexedAccess": true,
18
+ "exactOptionalPropertyTypes": true,
19
+ "strict": true,
20
+ "verbatimModuleSyntax": true,
21
+ "isolatedModules": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "moduleDetection": "force",
24
+ "skipLibCheck": true
25
+ }
26
+ }