{"id":17189799,"url":"https://github.com/synvox/api","last_synced_at":"2025-02-23T23:31:34.075Z","repository":{"id":44073062,"uuid":"208171285","full_name":"Synvox/api","owner":"Synvox","description":"Simple data loading for React","archived":true,"fork":false,"pushed_at":"2023-01-04T10:19:19.000Z","size":1635,"stargazers_count":35,"open_issues_count":8,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-02-19T22:14:21.420Z","etag":null,"topics":["axios","dataloading","react","suspense"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@synvox/api","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Synvox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-09-13T00:50:23.000Z","updated_at":"2024-10-23T00:37:10.000Z","dependencies_parsed_at":"2023-02-02T08:15:18.636Z","dependency_job_id":null,"html_url":"https://github.com/Synvox/api","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Synvox%2Fapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Synvox%2Fapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Synvox%2Fapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Synvox%2Fapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Synvox","download_url":"https://codeload.github.com/Synvox/api/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240395717,"owners_count":19794572,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axios","dataloading","react","suspense"],"created_at":"2024-10-15T01:12:32.753Z","updated_at":"2025-02-23T23:31:33.609Z","avatar_url":"https://github.com/Synvox.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `@synvox/api`\n\n![Travis (.org)](https://img.shields.io/travis/synvox/api)\n![Codecov](https://img.shields.io/codecov/c/github/synvox/api)\n![Bundle Size](https://badgen.net/bundlephobia/minzip/@synvox/api)\n![License](https://badgen.net/npm/license/@synvox/api)\n[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Synvox/api.svg?logo=lgtm\u0026logoWidth=18)](https://lgtm.com/projects/g/Synvox/api/context:javascript)\n\nSimple HTTP calls in React using Suspense.\n\n```\nnpm i @synvox/api axios\n```\n\n## CodeSandbox\n\n[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/goofy-albattani-oq838?fontsize=14)\n\n## Features\n\n- Wrapper around `axios`. Pass in an `axios` instance of your choosing\n- Small interface\n  - `useApi` a suspense compatible hook for loading data\n  - `api` a wrapper around axios\n  - `touch(...keys: string[])` to refetch queries\n  - `defer\u003cT\u003e(() =\u003e T, defaultValue: T): {data: T, loading:boolean}` to defer an HTTP call\n  - `preload(() =\u003e any): Promise\u003cvoid\u003e` to preload an HTTP call\n- Run any `GET` request through Suspense\n- Refresh requests without flickering\n- De-duplicates `GET` requests to the same url\n- Caches urls while they're in use and garbage collects them when they are not.\n- Can be used in conditions and loops\n- Easy integration with websockets and SSE for real-time apps\n- Well tested and and written in Typescript\n- Tiny\n\n## Basic Example\n\n```js\nimport { createApi } from '@synvox/api';\nimport axios from 'axios';\n\nconst { useApi } = createApi(\n  axios.create({\n    baseURL: 'https://your-api.com',\n    headers: {\n      'Authorization': 'Bearer your-token-here'\n    }\n  })\n);\n\nexport useApi;\n\n// then elsewhere:\n\nimport { useApi } from './api'\n\nfunction Post({postId}) {\n  const api = useApi();\n\n  const user = api.users.me.get(); // GET https://your-api.com/users/me\n  const post = api.posts[postId].get(); // GET https://your-api.com/posts/{postId}\n  const comments = api.comments.get({postId: post.id}); // GET https://your-api.com/comments?post_id={postId}\n\n  const authorName = post.authorId === user.id\n    ? 'You'\n    : api.users[post.authorId].get().name// GET https://your-api.com/users/{post.authorId}\n\n  return \u003c\u003e\n    \u003ch2\u003e{post.title} by {authorName}\u003c/h2\u003e\n    \u003cp\u003e{post.body}\u003c/p\u003e\n    \u003cul\u003e\n      {comments.map(comment=\u003e\u003cli key={comment.id}\u003e{comment.body}\u003c/li\u003e)}\n    \u003c/ul\u003e\n  \u003c/\u003e;\n}\n\n```\n\n## The `useApi` hook\n\n`useApi` returns a `Proxy` that builds an axios request when you call it. For example:\n\n```js\nimport { createApi } from '@synvox/api';\nimport axios from 'axios';\n\nconst { useApi } = createApi(axios);\n\n// in a component:\nconst api = useApi();\n\nconst users = api.users(); // calls GET /users\nconst notifications = api.notifications.get(); // calls GET /notifications, defaults to `get` when no method is specified.\n\nconst userId = 1;\nconst comments = api.users({ userId: 1 }); // calls GET /users?user_id=1\n\nconst projectId = 2;\nconst project = api.projects[projectId](); // calls GET /projects/2\n\nconst userProject = api.users[userId].projects[projectId]({ active: true }); // calls GET /users/1/projects/2?active=true\n```\n\n### Calling `api`\n\n```ts\napi.path[urlParam](params: object, config?: AxiosConfig) as Type\n//  |    |         |               |__ axios options like `data` and `headers`\n//  |    |         |__ query params (uses query-string under the hood so arrays work)\n//  |    |__ url params\n//  \\__ the url path\n```\n\n### `useApi` and the laws of hooks\n\nYou cannot wrap a hook in a condition or use it in a loop, but the `api` object is not a hook, so feel free to use it wherever data is needed.\n\n```js\nconst api = useApi();\n\nconst users = shouldLoadUsers ? api.users() : [];\n\nreturn (\n  \u003c\u003e\n    {users.map(user =\u003e (\n      \u003cdiv key={user.id}\u003e\n        {user.name}: {api.stars.count({ userId: user.id })}\n      \u003c/div\u003e\n    ))}\n  \u003c/\u003e\n);\n```\n\n### Refetching\n\nCall `touch` to refetch queries by url fragment(s).\n\n```js\nimport { createApi } from '@synvox/api';\nimport axios from 'axios';\n\nconst { useApi, touch } = createApi(axios);\n\n// in a component\nconst api = useApi();\nconst [commentBody, setCommentBody] = useState('');\n\nasync function submit(e) {\n  e.preventDefault();\n\n  // notice you can specify a method when making a call\n  await api.comments.post(\n    {},\n    {\n      data: {\n        body: commentBody,\n      },\n    }\n  );\n  // when used outside a render phase, api returns an AxiosPromise\n\n  await touch('comments', 'users');\n\n  setCommentBody('');\n}\n\nreturn \u003cform onSubmit={submit}\u003e// Component stuff\u003c/form\u003e;\n```\n\nThe `touch` function will find all the used requests that contain the word(s) given to touch and run those requests again in the background, only updating the components when all the requests are completed. This helps a ton with flickering and race conditions.\n\nBecause `touch` is not a hook, it can be used outside a component in a websocket handler or a SSE listener to create real-time experiences.\n\n```js\nimport { touch } from './api';\n\nconst sse = new EventSource('/events');\n\nsse.addEventListener('update', e =\u003e {\n  // assume e.data is {touches: ['messages', 'notifications']}\n  touch(...e.data.touches);\n});\n```\n\n## Using `api` outside a component\n\nWhen the api object is used outside a component as its rendering, it will return an `axios` call to that url.\n\n```js\nimport { api } from './api';\n\nexport async function logout() {\n  // notice you can specify a method like `post` when making a call\n  await api.logout.post();\n}\n```\n\n## Preloading (and avoiding waterfall requests)\n\nSuspense will wait for promises to fulfill before resuming a render which means requests are _not_ loaded parallel. While this is fine for many components, you may want to start the loading of many requests at once. To do this call `preload`:\n\n```js\nimport { preload, useApi } from './api';\n\nfunction Component() {\n  const api = useApi();\n\n  // use the same way you would in a render phase\n  preload(() =\u003e api.users());\n  preload(() =\u003e api.posts());\n\n  // suspend for /users\n  const users = api.users();\n\n  // suspend for /posts, but the promise for posts will have\n  // already been created in the preload call above.\n  const posts = api.posts();\n\n  return (\n    \u003cnav\u003e\n      \u003ca\n        href=\"/tasks\"\n        onMouseDown={() =\u003e {\n          // use preload in a handler if you want\n          preload(() =\u003e {\n            // works with multiple calls\n            const user = api.users.me();\n            const tasks = api.tasks({ userId: user.id });\n          });\n        }}\n      \u003e\n        Tasks\n      \u003c/a\u003e\n    \u003c/nav\u003e\n  );\n}\n```\n\n## Deferring Requests (make request, but don't suspend)\n\nIf you need to make a request but need to defer until after the first render, then use `defer`:\n\n```js\nimport { defer } from '@synvox/api';\n\nfunction Component() {\n  const api = useApi();\n\n  const { data: users, loading } = defer(() =\u003e api.users(), []);\n\n  if (loading) return \u003cSpinner /\u003e;\n  return \u003cUsersList users={users} /\u003e;\n}\n```\n\nThis still subscribes the component to updates from `touch`, request de-duplication, and garbage collection.\n\n## Binding Links\n\nYou can build graph-like structures with `useApi` by adding a modifier. Pass in a `modifier` to `createApi` to build custom link bindings:\n\n```js\n// Transforms responses like {'@links': {comments: '/comments?post_id=123' }} into\n// an object where data.comments will load /comments?post_id=123\n\nfunction bindLinks(object: any, loadUrl: (url: string) =\u003e unknown) {\n  if (!object || typeof object !== 'object') return object;\n  const { '@links': links } = object;\n  if (!links) return object;\n\n  const returned: any = Array.isArray(object) ? [] : {};\n\n  for (let [key, value] of Object.entries(object)) {\n    if (value \u0026\u0026 typeof value === 'object') {\n      returned[key] = bindLinks(value, loadUrl);\n    } else returned[key] = value;\n  }\n\n  if (!links) return returned;\n\n  for (let [key, url] of Object.entries(links)) {\n    if (!object[key]) {\n      Object.defineProperty(returned, key, {\n        get() {\n          return loadUrl(url as string);\n        },\n        enumerable: false,\n        configurable: false,\n      });\n    }\n  }\n\n  return returned;\n}\n\nconst { useApi } = createApi(axios, {\n  modifier: bindLinks,\n});\n```\n\n## Defining nested dependencies\n\nSay you call `/comments` which returns `Comment[]` and want each `Comment` to be loaded into the cache individually so calling `/comments/:id` doesn't make another request. You can do this by setting a deduplication strategy.\n\n```js\n// will update the cache for all all `{\"@url\": ...} objects\nfunction deduplicationStrategy(item: any): { [key: string]: any } {\n  if (!item || typeof item !== 'object') return {};\n  if (Array.isArray(item))\n    return item\n      .map(deduplicationStrategy)\n      .reduce((a, b) =\u003e ({ ...a, ...b }), {});\n\n  const result: { [key: string]: any } = {};\n\n  for (let value of Object.values(item)) {\n    Object.assign(result, deduplicationStrategy(value));\n  }\n\n  if (item['@url']) {\n    result[item['@url']] = item;\n  }\n\n  return result;\n}\n\nconst { useApi, api, touch, reset, preload } = createApi(axios, {\n  modifier: bindLinks,\n  deduplicationStrategy: (item: any) =\u003e {\n    const others = deduplicationStrategy(item);\n    return others;\n  },\n});\n```\n\n## Case Transformations\n\nYou can optionally specify a case transformation for request bodies, response bodies, and urls.\n\n```js\ncreateApi(axios, {\n  requestCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',\n  responseCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',\n  urlCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',\n});\n```\n\n## Saving and Restoring\n\nTo save the cache call `save`:\n\n```js\nconst { save, restore } = createApi(axios);\nlocalStorage.__cache = JSON.stringify(save());\n```\n\nTo restore the cache call `restore`:\n\n```js\nconst { save, restore } = createApi(axios);\nrestore(window.data__from__SSR);\n```\n\n## Retries\n\nSet `retryCount` to specify how many times failing `GET` requests should be retried. Requests are delayed by `1s` and double for each retry but will not delay longer than `30s`. E.g. `1s, 2s, 4s, 8s, ..., 30s`\nRetrying only applies to `GET` requests called in a render.\n\n```js\ncreateApi(axios, { retryCount: 10 });\n```\n\n## Why not just a `useEffect` hook or Redux?\n\n[See Comparison](comparison.md)\n\n### Obligatory Notice about Suspense for data loading\n\nThe React team has asked that we do not build on `react-cache` until it is stable, but that doesn't mean we can't experiment with an implementation of our own Suspense compatible cache until `react-cache` is stable.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsynvox%2Fapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsynvox%2Fapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsynvox%2Fapi/lists"}