{"id":50427062,"url":"https://github.com/poki/rx-api","last_synced_at":"2026-05-31T11:30:50.690Z","repository":{"id":355742463,"uuid":"209750204","full_name":"poki/rx-api","owner":"poki","description":"Reactive API system for redux-observable","archived":false,"fork":false,"pushed_at":"2026-05-05T02:47:31.000Z","size":520,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-05-05T04:33:08.161Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/poki.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-09-20T09:08:33.000Z","updated_at":"2026-05-05T02:47:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/poki/rx-api","commit_stats":null,"previous_names":["poki/rx-api"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/poki/rx-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/poki%2Frx-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/poki%2Frx-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/poki%2Frx-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/poki%2Frx-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/poki","download_url":"https://codeload.github.com/poki/rx-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/poki%2Frx-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33730240,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2026-05-31T11:30:49.797Z","updated_at":"2026-05-31T11:30:50.685Z","avatar_url":"https://github.com/poki.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rx-api ✨\n\nReactive API system for `redux-observable`.\n\n## Features\n\nWhat does rx-api give you:\n\n- Access to all your APIs through redux actions\n- Consistent API state structure without the boilerplate\n- Imbued with the power of RxJS\n\nWhat does rx-api not do:\n\n- Specific implementation of how to handle API responses\n\n## Dependencies\n\n1. rxjs\n2. redux\n3. redux-observable\n\n## Installation\n\n`$ yarn add @poki/rx-api`\n\n### Basic setup\n\n1. Create an API epic\n\n```js\nimport { createApiEpic } from '@poki/rx-api';\n\nexport const getGames = createApiEpic(\n\t'games/all', // identifier\n\tcallApi =\u003e callApi({ url: 'https://api.com/games', method: 'GET' }),\n);\n```\n\n2. Register the epic with redux-observable and create the reducer\n\n```js\nimport { createApiReducer } from '@poki/rx-api';\nimport { createStore, combineReducers } from 'redux';\nimport { createEpicMiddleware, combineEpics } from 'redux-observable';\n\nimport { getGames } from './epics';\n\nconst epicMiddleware = createEpicMiddleware();\n\nconst apiReducerKey = 'rx-api';\n\nexport const store = createStore(\n\tcombineReducers({\n\t\t// A: Add the reducer, ensure you pass the key\n\t\t[apiReducerKey]: createApiReducer(apiReducerKey),\n\t}),\n\tapplyMiddleware(\n\t\tepicMiddleware,\n\t),\n);\n\n// B: Register your api epics\nepicMiddleware.run(getGames);\n```\n\n3. Call that epic\n\n\n```js\nimport { useDispatch } from 'redux';\n\nimport { getGames } from './epics';\n\nconst Component = () =\u003e {\n\tconst dispatch = useDispatch();\n\n\tdispatch(getGames.fetch());\n};\n```\n\n### Storing API results in state\n\n```js\nimport { getGames } from './epics';\n\nfunction gameReducer(state, action) {\n\tif (action.type === getGames.success) {\n\t\tconst ajaxResult = action.payload.result;\n\t\treturn {\n\t\t\t...state,\n\t\t\tgames: ajaxResult.response.games,\n\t\t};\n\t}\n}\n```\n\n### Retrieving API call status\n\n```js\nimport { useSelector, useDispatch } from 'redux';\nimport { useSelectApiStatus } from '@poki/rx-api'; // Or selectApiStatus if you use selectors\n\nimport { getGames } from './epics';\n\nconst Component = () =\u003e {\n\tconst status = useSelectApiStatus(getGames.id);\n\tconst games = useSelector(state =\u003e state.game.games);\n\tconst dispatch = useDispatch();\n\n\t// Call the API\n\tdispatch(getGames.fetch());\n\n\t// Show the results\n\tif (status.pending) {\n\t\treturn `Pending... (${status.progress * 100}%)`;\n\t} else if (status.error) {\n\t\treturn `Error occured during getGames: ${error}`;\n\t}\n\n\treturn games;\n};\n```\n\n### Passing data to epic\n\n```js\nexport const getGamesById = createApiEpic(\n\t'games/by_id', // identifier\n\t(callApi, options) =\u003e callApi({ url: `https://api.com/games/${options.id}`, method: 'GET' }),\n);\n\n// Fetch example\ndispatch(getGamesById.fetch({ id: 1337 }));\n```\n\n### Epic-level callbacks\n\n```js\nimport { merge } from 'rxjs';\nimport { tap, ignoreElements } from 'rxjs/operators';\n\nexport const getGamesById = createApiEpic(\n\t'games/by_id', // identifier\n\t(callApi, options) =\u003e callApi({ url: `https://api.com/games/${options.id}`, method: 'GET' }),\n\t({ success$, error$, cancel$, progress$ }) =\u003e merge(\n\t\tsuccess$.pipe(\n\t\t\t// -\u003e in: getGamesById.success action\n\t\t\ttap(action =\u003e console.info('API call successful', action)),\n\t\t\tignoreElements(), // -\u003e out: nothing, Ensure we don't duplicate our action\n\t\t),\n\t\terror$.pipe(\n\t\t\t// -\u003e in: getGamesById.error action\n\t\t\ttap(action =\u003e console.info('API call error', action)),\n\t\t\tignoreElements(), // -\u003e out: nothing, Ensure we don't duplicate our action\n\t\t),\n\t\tprogress$.pipe(\n\t\t\t// -\u003e in: getGamesById.progress action\n\t\t\ttap(action =\u003e console.info('API call progress update', action)),\n\t\t\tignoreElements(), // -\u003e out: nothing, Ensure we don't duplicate our action\n\t\t),\n\t\tcancel$.pipe(\n\t\t\t// -\u003e in: getGamesById.cancel action\n\t\t\ttap(action =\u003e console.info('API call canceled', action)),\n\t\t\tignoreElements(), // -\u003e out: nothing, Ensure we don't duplicate our action\n\t\t),\n\t),\n);\n```\n\n### Action-level callbacks\n\n```js\ndispatch(\n\tgetGamesById.fetch(\n\t\t{ id: 1337 },\n\t\t({ success$ }) =\u003e success$.pipe(\n\t\t\t// [...] etc. See epic-level callbacks.\n\t\t),\n\t),\n);\n```\n\n# Advanced\n\n### Setting up authorized API routes\n\nWe can create a wrapper for createApiEpic that injects authorization headers on every action created with it as such:\n\n```js\nexport const createAuthorizedApiEpic = (id, handler, getCBStream) =\u003e {\n\t// Return the original createApiEpic\n\treturn createApiEpic(\n\t\tid,\n\t\t// Wrap the handler\n\t\t(callApi, options = {}, state) =\u003e {\n\t\t\t// Select the access token from redux\n\t\t\tconst accessToken = selectAccessToken(state);\n\n\t\t\t// Return the original handler\n\t\t\treturn handler(callApi, options, state).pipe(\n\t\t\t\tmap(action =\u003e {\n\t\t\t\t\t// Inject authorization header in any callApi actions\n\t\t\t\t\tif (action.type === callApi.type) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...action,\n\t\t\t\t\t\t\tpayload: {\n\t\t\t\t\t\t\t\t...(action.payload || {}),\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t...(action.payload.headers || {}),\n\t\t\t\t\t\t\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pass through any other actions directly\n\t\t\t\t\treturn action;\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\t\tgetCBStream,\n\t);\n};\n```\n\n### Set up automatic token refreshing\n\nExpanding on the above:\n\n```js\n// Create an apiEpic for refreshing authorization tokens\nexport const refreshAuth = createApiEpic(\n\t'session/refresh',\n\t(callApi, { refreshToken }) =\u003e callApi({\n\t\turl: 'https://api.com/authorization',\n\t\tmethod: 'POST',\n\t\theaders: { 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ refresh_token: refreshToken }),\n\t}),\n\t({ success$ }) =\u003e (\n\t\tsuccess$.pipe(\n\t\t\tswitchMap(({ payload: { result: { response } } }) =\u003e (\n\t\t\t\tof(\n\t\t\t\t\tsetAccessToken({ accessToken: response.access_token }),\n\t\t\t\t\tsetTokenTTL({ ttl: response.ttl }),\n\t\t\t\t)\n\t\t\t)),\n\t\t)\n\t),\n);\n\n// Expanded createAuthorizedApiEpic that calls refreshAuth if necessary before handling the original action\nexport const createAuthorizedApiEpic = (id, handler, getCBStream) =\u003e {\n\tconst authorizedHandler = (callApi, options = {}, state) =\u003e {\n\t\t// Helper method to create callApi based on access token\n\t\tconst createCallApi = accessToken =\u003e (\n\t\t\t// Execute original handler, and pipe the result\n\t\t\thandler(callApi, options, state).pipe(\n\t\t\t\t// Adjust resulting action if necessary\n\t\t\t\tmap(action =\u003e {\n\t\t\t\t\tif (action.type === callApi.type) {\n\t\t\t\t\t\t// Inject authorization header\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t...action,\n\t\t\t\t\t\t\tpayload: {\n\t\t\t\t\t\t\t\t...(action.payload || {}),\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t...(action.payload.headers || {}),\n\t\t\t\t\t\t\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pass through any other actions directly\n\t\t\t\t\treturn action;\n\t\t\t\t}),\n\t\t\t)\n\t\t);\n\n\t\tconst expires = selectTokenExpires(state);\n\t\tif (expires \u003c Date.now()) {\n\t\t\tconst refreshToken = selectRefreshToken(state);\n\n\t\t\t// Refresh before handling original api request\n\t\t\treturn of(\n\t\t\t\trefreshAuth.fetch(\n\t\t\t\t\t{ refreshToken },\n\t\t\t\t\t({ success$ }) =\u003e (\n\t\t\t\t\t\tsuccess$.pipe(\n\t\t\t\t\t\t\tswitchMap(({ payload: { result: { response } } }) =\u003e createCallApi(response.access_token)),\n\t\t\t\t\t\t)\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// No refreshing necessary\n\t\tconst accessToken = selectAccessToken(state);\n\t\treturn createCallApi(accessToken);\n\t};\n\n\treturn createApiEpic(id, authorizedHandler, getCBStream);\n};\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpoki%2Frx-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpoki%2Frx-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpoki%2Frx-api/lists"}