@alistt69/create-api 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/README.md +299 -137
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <table width="100%">
2
2
  <tr>
3
3
  <td width="190" align="center">
4
- <img src="assets/alistt69-packages-logo.svg" alt="Logo" width="170" height="170" style="margin-top: 50px;" />
4
+ <img src="assets/alistt69-packages-logo.svg" alt="@alistt69 packages logo" width="160" height="160" />
5
5
  </td>
6
6
  <td>
7
7
  <h1>@alistt69/create-api</h1>
8
8
 
9
9
  > **One helper. No extra.**
10
- > A lightweight createApi inspired by **RTKQ** — with query, lazy query and mutation hooks, built-in cache utilities, stale data handling and a tiny, focused API.
10
+ > A lightweight createApi inspired by **RTKQ** — with query, lazy query and mutation hooks & endpoint controllers, built-in cache utilities, stale data handling and a tiny, focused API.
11
11
 
12
12
  [![npm version](https://img.shields.io/npm/v/@alistt69/create-api.svg)](https://www.npmjs.com/package/@alistt69/create-api)
13
13
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
@@ -19,220 +19,382 @@
19
19
  </tr>
20
20
  </table>
21
21
 
22
- ## 🚀 Demo
22
+ ---
23
23
 
24
- _[Live Demo](https://create-api-demo.vercel.app/)_
25
- <br />
26
- _Sandbox is coming soon..._
24
+ <p align="center">
25
+ <a href="#install">Install</a>
26
+ &middot; <a href="#quick-start">Quick Start</a>
27
+ &middot; <a href="#the-shape">The Shape</a>
28
+ &middot; <a href="#cache">Cache</a>
29
+ &middot; <a href="#imperative-api">Imperative API</a>
30
+ &middot; <a href="#controller">Controller</a>
31
+ </p>
27
32
 
28
- ## ✨ Overview
33
+ ---
29
34
 
30
- `@alistt69/create-api` is a lightweight [RTK Query-inspired](https://redux-toolkit.js.org/rtk-query/overview) helper for React apps that need a clean createApi experience without Redux.
35
+ ## Why this exists
31
36
 
32
- It provides query, lazy query and mutation hooks, cache helpers and stale data support in a compact API designed for everyday app needs.
37
+ `@alistt69/create-api` is for React projects that like the ergonomics of
38
+ RTK Query, but do not want to introduce Redux just to fetch data.
33
39
 
34
- ## 🔧 What’s inside
40
+ It gives you a compact `createApi` workflow:
35
41
 
36
- | Feature | Purpose |
37
- | -------------------- | ------------------------------------ |
38
- | Query hooks | Fetch data with generated hooks |
39
- | Lazy query hooks | Trigger queries manually when needed |
40
- | Mutation hooks | Handle writes with generated hooks |
41
- | fetchBaseQuery | Ready-to-use HTTP baseQuery built on fetch |
42
- | Cache utils | Read and patch cached data manually |
43
- | Stale time support | Reuse cached data before refetching |
44
- | Keep unused data for | Control cache lifetime after unmount |
45
-
46
- ## 🎯 Why use it?
47
-
48
- This package is specified for projects with no Redux.
42
+ ```txt
43
+ define endpoints -> get typed hooks -> read/write cache -> refetch when stale
44
+ ```
49
45
 
50
- It gives you a RTKQ-similar API model and developer experience, while staying smaller, simpler and focused on the most common data-fetching needs.
46
+ Use it when you want:
51
47
 
52
- Do whatever you want and however you want with:
53
- * generated hooks
54
- * predictable cache behavior
55
- * simple manual cache updates
56
- * a focused API without a larger state layer
57
- * a built-in `fetchBaseQuery` for managing your HTTP requests
48
+ | You need | You get |
49
+ | --- | --- |
50
+ | Generated React hooks | `useGetPostQuery`, `useLazyGetPostQuery`, `useUpdatePostMutation` |
51
+ | A small HTTP layer | `fetchBaseQuery`, built on native `fetch` |
52
+ | Cache reads and patches | `getQueryData`, `setQueryData`, `updateQueryData` |
53
+ | Stale data handling | `staleTime`, `keepUnusedDataFor`, `refetchOnMount` |
54
+ | Manual orchestration | `api.endpoints.*.initiate`, `select`, `subscribe` |
55
+ | Store-based usage | `createController` from the controller subpath |
58
56
 
59
- ## 📦 Requirements
60
- * Node.js 18 or higher
61
- * React 16.8 or higher
57
+ ## Install
62
58
 
63
- ## 🔥 Install
64
59
  ```bash
65
60
  npm i @alistt69/create-api
66
61
  ```
67
62
 
68
- ## ⚡ Quick example
69
- ```typescript jsx
63
+ Requirements:
64
+
65
+ | Runtime | Version |
66
+ | --- | --- |
67
+ | Node.js | `>=18` |
68
+ | React | `>=16.8` |
69
+
70
+ ## Quick Start
71
+
72
+ Create an API once:
73
+
74
+ ```tsx
70
75
  import { createApi, fetchBaseQuery } from '@alistt69/create-api';
71
76
 
72
77
  const api = createApi({
73
- baseQuery: fetchBaseQuery({
74
- baseUrl: 'https://example.com/api',
78
+ baseQuery: fetchBaseQuery({
79
+ baseUrl: 'https://example.com/api',
80
+ }),
81
+
82
+ endpoints: (builder) => ({
83
+ getPost: builder.query({
84
+ query: (id: string) => ({
85
+ url: `/posts/${id}`,
86
+ }),
75
87
  }),
76
88
 
77
- endpoints: (builder) => ({
78
- getPost: builder.query({
79
- query: (id) => ({
80
- url: `/posts/${id}`,
81
- }),
82
- }),
83
-
84
- updatePost: builder.mutation({
85
- query: ({ id, title }) => ({
86
- url: `/posts/${id}`,
87
- method: 'PATCH',
88
- body: { title },
89
- }),
90
- }),
89
+ updatePost: builder.mutation({
90
+ query: ({ id, title }: { id: string; title: string }) => ({
91
+ url: `/posts/${id}`,
92
+ method: 'PATCH',
93
+ body: { title },
94
+ }),
91
95
  }),
96
+ }),
92
97
  });
93
98
  ```
94
99
 
95
- ## 🛠️ Usage
96
- ```typescript jsx
100
+ Then use the generated hooks:
101
+
102
+ ```tsx
97
103
  function Post() {
98
- const { data, isLoading } = api.useGetPostQuery('1');
99
- const [updatePost] = api.useUpdatePostMutation();
104
+ const { data, isLoading, refetch } = api.useGetPostQuery('1');
105
+ const [updatePost, updateState] = api.useUpdatePostMutation();
100
106
 
101
- if (isLoading) return <div>Loading...</div>;
107
+ if (isLoading) {
108
+ return <div>Loading...</div>;
109
+ }
102
110
 
103
111
  return (
104
- <button onClick={() => updatePost({ id: '1', title: 'Updated' })}>
105
- {data?.title}
106
- </button>
112
+ <section>
113
+ <h2>{data?.title}</h2>
114
+
115
+ <button
116
+ disabled={updateState.isLoading}
117
+ onClick={() => updatePost({ id: '1', title: 'Updated' })}
118
+ >
119
+ Update
120
+ </button>
121
+
122
+ <button onClick={() => refetch()}>
123
+ Refetch
124
+ </button>
125
+ </section>
107
126
  );
108
127
  }
109
128
  ```
110
129
 
111
- ## 🌐 fetchBaseQuery
130
+ ## The Shape
112
131
 
113
- `fetchBaseQuery` is a small ready-to-use `baseQuery` built on top of the native `fetch` API.
132
+ The package is intentionally small, but the surface area covers the core
133
+ data-fetching loop.
114
134
 
115
- It supports:
116
- - `baseUrl`
117
- - `params`
118
- - `headers`
119
- - `prepareHeaders`
120
- - `timeout`
121
- - `responseHandler`
122
- - `validateStatus`
123
- - `fetchFn`
135
+ ### Query hooks
124
136
 
125
- ### Example
137
+ ```tsx
138
+ const result = api.useGetPostQuery('1', {
139
+ enabled: true,
140
+ refetchOnMount: true,
141
+ });
142
+ ```
143
+
144
+ Query state includes:
145
+
146
+ | Field | Meaning |
147
+ | --- | --- |
148
+ | `data` | Last successful value |
149
+ | `error` | Last failed value |
150
+ | `status` | `uninitialized`, `pending`, `fulfilled` or `rejected` |
151
+ | `isLoading` | First request is in progress |
152
+ | `isFetching` | Any request is in progress, including background refetch |
153
+ | `isSuccess` | Last known state is successful |
154
+ | `isError` | Last known state is failed |
155
+ | `fulfilledAt` | Timestamp of the last fulfilled request |
156
+ | `requestId` | Active or last request id |
157
+
158
+ ### Lazy query hooks
159
+
160
+ ```tsx
161
+ const [loadPost, post] = api.useLazyGetPostQuery();
162
+
163
+ await loadPost('1');
164
+
165
+ post.refetch();
166
+ ```
167
+
168
+ Lazy queries are useful when the request should start from a user action,
169
+ a modal opening, a route transition or another explicit event.
170
+
171
+ ### Mutation hooks
172
+
173
+ ```tsx
174
+ const [updatePost, updateState] = api.useUpdatePostMutation();
175
+
176
+ await updatePost({ id: '1', title: 'Updated' });
177
+
178
+ updateState.reset();
179
+ ```
180
+
181
+ Mutation state tracks the latest trigger. This keeps UI behavior predictable
182
+ when several mutation calls overlap.
183
+
184
+ ## `fetchBaseQuery`
185
+
186
+ `fetchBaseQuery` is a ready-to-use `baseQuery` built on top of native `fetch`.
187
+ It handles URLs, params, JSON bodies, headers, timeouts and response parsing.
126
188
 
127
189
  ```tsx
128
190
  import { createApi, fetchBaseQuery } from '@alistt69/create-api';
129
191
 
130
192
  const api = createApi({
131
- baseQuery: fetchBaseQuery({
132
- baseUrl: 'https://example.com/api',
133
- prepareHeaders: (headers) => {
134
- headers.set('authorization', 'Bearer token');
135
- return headers;
136
- },
137
- }),
138
- endpoints: (builder) => ({
139
- getTickets: builder.query({
140
- query: ({ page }) => ({
141
- url: '/tickets',
142
- params: { page },
143
- }),
144
- }),
193
+ baseQuery: fetchBaseQuery({
194
+ baseUrl: 'https://example.com/api',
195
+ timeout: 10_000,
196
+ prepareHeaders: (headers) => {
197
+ headers.set('authorization', 'Bearer token');
198
+ return headers;
199
+ },
200
+ }),
201
+
202
+ endpoints: (builder) => ({
203
+ getTickets: builder.query({
204
+ query: ({ page }: { page: number }) => ({
205
+ url: '/tickets',
206
+ params: { page },
207
+ }),
145
208
  }),
209
+ }),
210
+ });
211
+ ```
212
+
213
+ Supported options:
214
+
215
+ | Option | Where | Purpose |
216
+ | --- | --- | --- |
217
+ | `baseUrl` | base query | Prefix all request URLs |
218
+ | `headers` | request | Add request-specific headers |
219
+ | `prepareHeaders` | base query | Modify headers before every request |
220
+ | `params` | request | Append query params |
221
+ | `paramsSerializer` | base query | Customize query string serialization |
222
+ | `body` | request | Send JSON, `FormData`, `Blob`, `URLSearchParams` or another fetch body |
223
+ | `timeout` | both | Abort slow requests |
224
+ | `responseHandler` | both | Parse as `json`, `text`, `content-type` or custom handler |
225
+ | `validateStatus` | both | Decide whether a response is success |
226
+ | `fetchFn` | base query | Use a custom fetch implementation |
227
+
228
+ Custom response handling:
229
+
230
+ ```tsx
231
+ downloadReport: builder.query({
232
+ query: () => ({
233
+ url: '/report',
234
+ responseHandler: (response) => response.blob(),
235
+ }),
146
236
  });
147
237
  ```
148
238
 
149
- ### Blob / custom response handling
150
- ```javascript
151
- downloadFile: builder.query({
152
- query: () => ({
153
- url: '/report',
154
- responseHandler: (response) => response.blob(),
239
+ ## Cache
240
+
241
+ Queries are cached by endpoint name and serialized argument.
242
+
243
+ ```tsx
244
+ const api = createApi({
245
+ baseQuery,
246
+ endpoints: (builder) => ({
247
+ getTicketById: builder.query({
248
+ query: (id: string) => ({ url: `/tickets/${id}` }),
249
+ serializeArgs: (id) => id,
250
+ staleTime: 2_000,
251
+ keepUnusedDataFor: 10_000,
155
252
  }),
156
- }),
253
+ }),
254
+ });
157
255
  ```
158
256
 
159
- ## ⚙️ Cache utils
160
- ``` typescript
257
+ Cache controls:
258
+
259
+ | Option | Behavior |
260
+ | --- | --- |
261
+ | `serializeArgs` | Builds the cache key for an endpoint argument |
262
+ | `staleTime` | Keeps fulfilled data fresh for automatic mount behavior |
263
+ | `keepUnusedDataFor` | Keeps unused cache alive after the last subscriber leaves |
264
+ | `refetchOnMount` | Controls whether cached data may refetch on mount |
265
+ | `enabled` | Disables automatic query execution while keeping manual `refetch` available |
266
+
267
+ Manual cache updates:
268
+
269
+ ```tsx
161
270
  api.util.getQueryData('getPost', '1');
162
- api.util.setQueryData('getPost', '1', { id: '1', title: 'Local title' });
271
+
272
+ api.util.setQueryData('getPost', '1', {
273
+ id: '1',
274
+ title: 'Local title',
275
+ });
276
+
163
277
  api.util.updateQueryData('getPost', '1', (prev) => ({
164
- ...prev,
165
- title: 'Patched title',
278
+ ...prev,
279
+ title: 'Patched title',
166
280
  }));
167
281
  ```
168
282
 
169
- ## 🧩 Imperative endpoints
283
+ ### Invalidation
284
+
285
+ Use endpoint-level invalidation for broad refetching:
286
+
287
+ ```tsx
288
+ editTicket: builder.mutation({
289
+ query: ({ id, title }) => ({
290
+ url: `/tickets/${id}`,
291
+ method: 'PATCH',
292
+ body: { title },
293
+ }),
294
+ invalidates: ['getTickets'],
295
+ });
296
+ ```
297
+
298
+ Use tag invalidation when you want to target specific cached records:
299
+
300
+ ```tsx
301
+ getTicketById: builder.query({
302
+ query: (id) => ({ url: `/tickets/${id}` }),
303
+ providesTags: (_result, id) => [`Ticket/${id}`],
304
+ }),
305
+
306
+ editTicket: builder.mutation({
307
+ query: ({ id, title }) => ({
308
+ url: `/tickets/${id}`,
309
+ method: 'PATCH',
310
+ body: { title },
311
+ }),
312
+ invalidatesTags: (_result, arg) => [`Ticket/${arg.id}`],
313
+ });
314
+ ```
315
+
316
+ ## Imperative API
170
317
 
171
- Each endpoint also exposes a small imperative API:
318
+ Every endpoint also exposes a small imperative API. It is useful when a query
319
+ lifecycle should live outside React components.
172
320
 
173
- ```typescript
174
- // query
321
+ ```tsx
175
322
  const request = api.endpoints.getPost.initiate('1');
176
323
 
177
324
  const data = await request.unwrap();
178
325
 
179
- request.refetch();
326
+ await request.refetch();
327
+
180
328
  request.unsubscribe();
181
329
  request.abort();
330
+ ```
331
+
332
+ You can also inspect query state directly:
182
333
 
183
- // mutation
334
+ ```tsx
184
335
  const state = api.endpoints.getPost.select('1');
336
+ ```
185
337
 
186
- const mutation = api.endpoints.editPost.initiate({
187
- id: '1',
188
- title: 'Updated',
338
+ And trigger mutations:
339
+
340
+ ```tsx
341
+ const mutation = api.endpoints.updatePost.initiate({
342
+ id: '1',
343
+ title: 'Updated',
189
344
  });
190
345
 
191
346
  await mutation.unwrap();
192
- mutation.abort();
193
347
 
348
+ mutation.abort();
194
349
  ```
195
- This is useful when query lifecycle should live outside React components, for example in MobX stores or other controller-style state layers.
196
350
 
197
- ## ⛓️‍💥 Controller
198
- For store-based usage you can import a small controller helper from the controller subpath:
199
- ```typescript
351
+ ## Controller
352
+
353
+ For store-based usage, import `createController` from the controller subpath:
354
+
355
+ ```tsx
200
356
  import { createController } from '@alistt69/create-api/controller';
201
357
 
202
358
  class TicketStore {
203
- private ticket = createController(api.endpoints.getTicketById);
204
- private editTicket = createController(api.endpoints.editTicket);
359
+ private ticket = createController(api.endpoints.getTicketById);
360
+ private editTicket = createController(api.endpoints.editTicket);
205
361
 
206
- load(id: string) {
207
- return this.ticket.run(id);
208
- }
362
+ load(id: string) {
363
+ return this.ticket.run(id);
364
+ }
209
365
 
210
- save(id: string, title: string) {
211
- return this.editTicket.run({ id, title });
212
- }
366
+ save(id: string, title: string) {
367
+ return this.editTicket.run({ id, title });
368
+ }
213
369
 
214
- get ticketData() {
215
- return this.ticket.state.data;
216
- }
370
+ get ticketData() {
371
+ return this.ticket.state.data;
372
+ }
217
373
 
218
- get isLoadingTicket() {
219
- return this.ticket.state.isLoading;
220
- }
374
+ get isLoadingTicket() {
375
+ return this.ticket.state.isLoading;
376
+ }
221
377
 
222
- get isSavingTicket() {
223
- return this.editTicket.state.isLoading;
224
- }
378
+ get isSavingTicket() {
379
+ return this.editTicket.state.isLoading;
380
+ }
225
381
 
226
- destroy() {
227
- this.ticket.dispose();
228
- this.editTicket.dispose();
229
- }
382
+ destroy() {
383
+ this.ticket.dispose();
384
+ this.editTicket.dispose();
385
+ }
230
386
  }
231
387
  ```
232
- The controller keeps endpoint state outside React, subscribes to cache updates, and releases its subscription with dispose().
233
388
 
234
- ## 📄 License
389
+ The controller keeps endpoint state outside React, subscribes to cache updates,
390
+ and releases its subscription with `dispose()`.
391
+
392
+ ## Demo
393
+
394
+ [Live demo](https://create-api-demo.vercel.app/)
395
+
396
+ Sandbox is coming soon.
235
397
 
236
- MIT — free and open for everyone.
398
+ ## License
237
399
 
238
- _See [LICENSE](./LICENSE)._
400
+ MIT. See [LICENSE](./LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alistt69/create-api",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight createApi with query/mutation/lazy hooks",
5
5
  "type": "module",
6
6
  "sideEffects": false,