{"id":25707700,"url":"https://github.com/jhsware/koa-restish","last_synced_at":"2026-06-10T22:31:32.743Z","repository":{"id":92670893,"uuid":"237025509","full_name":"jhsware/koa-restish","owner":"jhsware","description":"Server and client library allowing RESTish endpoints with batching and caching. An easy to learn and lightweight alternative to GraphQL.","archived":false,"fork":false,"pushed_at":"2024-03-01T14:43:15.000Z","size":58,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-01T05:20:47.069Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jhsware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2020-01-29T16:10:40.000Z","updated_at":"2021-10-05T06:53:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"64557613-1b5a-4a80-96bb-2e0a2c31d113","html_url":"https://github.com/jhsware/koa-restish","commit_stats":{"total_commits":59,"total_committers":1,"mean_commits":59.0,"dds":0.0,"last_synced_commit":"c3fb5df3b19d5196a862f1f695728bb864c07ec8"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/jhsware/koa-restish","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhsware%2Fkoa-restish","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhsware%2Fkoa-restish/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhsware%2Fkoa-restish/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhsware%2Fkoa-restish/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jhsware","download_url":"https://codeload.github.com/jhsware/koa-restish/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jhsware%2Fkoa-restish/sbom","scorecard":{"id":519158,"data":{"date":"2025-08-11","repo":{"name":"github.com/jhsware/koa-restish","commit":"c3fb5df3b19d5196a862f1f695728bb864c07ec8"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-20T02:32:32.622Z","repository_id":92670893,"created_at":"2025-08-20T02:32:32.622Z","updated_at":"2025-08-20T02:32:32.622Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34174148,"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-06-10T02:00:07.152Z","response_time":89,"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":"2025-02-25T08:38:34.340Z","updated_at":"2026-06-10T22:31:32.724Z","avatar_url":"https://github.com/jhsware.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# koa-restish\n\nRESTish is the natural evolution of REST APIs, with the addition of batch calls and client-side endpoint level caching. This bridges some of the gap between GraphQL and REST but at 1/10th of the size.\n\nThe library consists of a server-side and a client-side part. The client-side code is isomorphic, so it runs both in a browser and on Node.js.\n\nOne of the greatest points of confusion with REST was how to use the verbs GET, PUT, POST and DELETE (okay, the last one is probably the least ambiguous). In RESTish they are simply called create, query, update and delete.\n\nThe server-side library defines your endpoints. The syntax is very similar to ordinary express or koa routes. If you have exprience of REST you will feel right at home. This is where you integrate your API:s and data sources. You also implement your session handling at this level allowing your frontend application to access the RESTish API directly from the browser. Note: if you do server-side rendering, you need to pass you cookies to the RESTish API, see example.\n\nThe client-side library abstracts data fetching and handles endpoint caching. This allows the library to transparently share API-state across multiple components in a single page application. By sharing state through caching you don't have to worry about the performance penalty of repeating calls to the API. Once an endpoint has been cached the results will be returned immediately through a `Promise.resolve()`. This makes the code more readable with less dependency between components.\n\nBecause RESTish uses the mental model of a REST API, it is easy to learn and lowers the cost of migration by several orders of magnitude. This lightweight approach is a perfect candidate to run on a serverless platform, integrating your micro-services.\n\n## Changelog\nv0.4 deprecates properties `data` and `status` returned in the client response object  (will be removed in 1.0 release).\nUse `result` instead, it includes both body and status for each action. To set status codes, throw the custom errors available in `koa-restish/lib/errors`. See ___test___/server.js and ___test___/serverExpress.js for examples.\n\nNOTE: Version 0.3 client is incompatible with 0.4 server code and vice versa. Make sure you use the same version on both client and server. After 1.0 release, we will maintain backward compatibility.\n\n## A RESTish endpoint handler\nThe RESTish endpoint handler is passed six named parameters. It is up to the developer to implement all the code in these handlers. This gives maximum flexibility.\n\n- **URI** {string} - the RESTish URI used to access the endpoint (as passed from the client)\n- **query** {object} - query params passed from the client (unedifined in most cases not involving queries)\n- **sortBy** {object} - select sort order\n- **shape** {object} - a flat shape object consisting of a list of expressions defining what output data we want (note, it is up to the developer to implement this, there are helper methods in `purgeWithShape.js` to help you but they are currently experimental, check the tests and docs at the end of this README)\n- **params** {object} - params of the URIas defined in the RESTish endpoint paths `/content/:type` provides the type param\n- **ctx** {object} - contains the `request` and `response` object, also the `session` object if available.\n\nNote that both koa-restish and express-restish handles sending your data to the client. All you need to do is return your result.\n\n```js\nconst createHandler = async ({ URI, query, shape, params, data, ctx }) =\u003e {\n  // Your code here that returns data in some shape or form:\n  return {\n    type: params.type\n  }\n})\n\nrestish.create('/content/:type', createHandler)\n```\n\n## express-restish\nIf you use Express.js there is a RESTish router available in `express-restish.js`. The endpoint handlers look identical to that of KoaJS below but th code to hookup the RESTish router. You can look at the tests in `__test__/serverExpress.js`.\n\n```js\nimport RestishRouter from 'koa-restish/lib/express-restish'\nconst restish = new RestishRouter()\n\nrouter.post('/restish', express.json(), restish.routes());\nrouter.get('/restish', (req, res, next) =\u003e res.send('ok'));\n```\n\nThe express-restish endpoint handlers are passed the `request`, `response` and `session` (if available) objects in the `ctx` param to keep consistency with koa-restish.\n\n## Server-side code example using KoaJS\n\nindex.mjs\n```js\nimport koa from 'koa'\nimport logger from 'koa-logger'\nimport session from 'koa-session'\nimport koaRouter from 'koa-router'\nimport RestishRouter from 'koa-restish/lib/koa-restish'\nimport koaJSONBody from 'koa-json-body'\nimport { createSession, querySession, endSession, initUsers } from './types/session'\nimport { createContent, queryContent, updateContent, deleteContent } from './types/content'\n\nconst app = new koa();\nconst router = new koaRouter();\n\nconst SESSION_CONFIG = {\n  httpOnly: true,\n  maxAge: 86400000,\n  signed: false\n}\n\napp.use(logger((str, args) =\u003e {\n  console.log(str)\n}))\n\napp.use(session(SESSION_CONFIG, app))\n\nconst restish = new RestishRouter()\n\n// Endpoints to login, logout and fetch current user\nrestish.create('/session', createSession)\nrestish.query('/session', querySession)\nrestish.delete('/session', endSession)\n\n// Endpoints to CRUD content\nrestish.create('/:type', createContent)\nrestish.query('/:type/:id?', queryContent)\nrestish.update('/:type/:id', updateContent)\nrestish.delete('/:type/:id', deleteContent)\n\nrouter.post('/restish', koaJSONBody(), restish.routes());\nrouter.get('/restish', (ctx) =\u003e ctx.body = 'ok');\n\napp.use(router.routes());\napp.use(router.allowedMethods());\n\napp.listen(PORT, () =\u003e {\n  console.log('Server listening on: ' + PORT)\n})\n```\n\ntypes/session.mjs\n```js\nconst querySession = async ({ URI, query, shape, params, ctx }) =\u003e {\n  return ctx.session.currentUser\n})\n\nasync function createSession ({ URI, query, shape, params, data, ctx }) {\n  /* Your code here to validate and fetch user */\n  ctx.session.currentUser = { ... }\n  return ctx.session.currentUser\n})\n\nconst endSession = ({ URI, query, shape, params, ctx }) =\u003e {\n  delete ctx.session.currentUser\n}\n\nexport {\n  createSession,\n  querySession,\n  endSession\n}\n```\n\ntypes/content.mjs\n```js\nasync function queryContent ({ URI, query, shape, params, ctx }) {\n  if (params.id) { // Fetch object by ID\n    /* Your code here */\n    return obj\n  }\n  else { // Find objects by query\n    /* Your code here */\n    return resArray\n  }\n})\n\nasync function createContent ({ URI, data, shape, params, ctx }) {\n  /* Your code here */\n  return newObj\n})\n\nasync function updateContent ({ URI, data, shape, params, ctx }) {\n  /* Your code here */\n  return updatedObj\n})\n\nasync function deleteContent ({ URI, query, shape, params, ctx }) {\n  /* Your code here */\n})\n\nexport {\n  createContent,\n  queryContent,\n  updateContent,\n  deleteContent\n}\n```\n\n## Client-side code examples\n\n```js\nimport { ApiClient } from 'koa-restish'\n\nconst API_URI = 'http://localhost:3000/restish'\nconst apiClient = new ApiClient({ API_URI })\n\n// Fetch current user\nconst { result } = await apiClient.query({\n  URI: '/session'\n})\nconst { body, status } = result\n// status is the equivalent of HTTP status codes\n// 200 - OK\n\n// Fetch data from multiple endpoints in a single call\nconst query = {\n  query: {\n    section: 'investor'\n  }\n}\nconst { result } = await apiClient.query([\n  { URI: `/content/app/${appId}` }\n  { URI: '/content/page', query }\n])\nconst { body: app, status: statusApp } = result[0]\nconst { body: pages, status: statusPages = result[1]\n\n// Create an object\nconst { result } = await apiClient.create({\n  URI: '/content/page',\n  data: { ... },\n  // Invalidates all cached endpoints that start with /content/page\n  invalidate: ['/content/page']\n})\nconst { body, status } = result\n// status is the equivalent of HTTP status codes\n// 201 - Created\n```\n\n## Server-side rendering\n\nPassing cookies to RESTish API in Koajs.\n\n```js\nimport axios from 'axios'\nimport { ApiClient } from 'koa-restish'\n\nconst API_URI = process.env.API_URI || 'http://127.0.0.1:3000/restish'\n\napp.use(async (ctx) =\u003e {\n  const headers = {}\n  if (ctx.headers['cookie']) {\n    headers['Cookie'] = ctx.headers['cookie']\n  }\n\n  // A new axios instance needs to be instantiated for every call so\n  // we don't leak the cookie secret.\n  const axiosInstance = axios.create({\n    headers\n  });\n\n  const apiClient = new ApiClient({\n    API_URI,\n    // Passing an axios instance with the cookie set to get client session\n    axios: axiosInstance\n  })\n\n  /**\n   * Your server-side rendering code goes here...\n   * \n   * const res = await apiClient.query(...)\n   * */\n})\n```\n\n## Recommended folder structure\n\n```js\nindex.js      // reads .env file and calls app.listen\nserver.js     // configure your routes, CORS, session handling\nendpoints/    // callbacks for OAuth2 etc.\napis/         // contains all your RESTish integrations with external APIs\n  serviceX.js // probaly mounted as '/api/serviceX'\n  serviceY.js // probaly mounted as '/api/serviceY'\ntypes/        // contains all your RESTish data manipulation endpoints organised by path\n  content.js  // content object CRUD  '/content/:type/...'\n  user.js     // user CRUD '/user/...'\n  session.js  // CRUD for session handling '/session/...'\n__tests__/    // tests for RESTish handlers etc.\n```\n\n## Writing tests\nTo call a RESTish handler in your tests you use the following signature:\n```js\nconst URI = '/path/to/endpoint'\nconst query = {} // Normally passed by client (optional)\nconst params = {} // Normally determined by RESTish router (optional)\nconst data = {} // only for write operations (optional)\nconst ctx = { request, response, session } // Only needed if you access them\nhandler({ URI, query, shape, params, data, ctx })\n```\n\n## Developer notes:\n\nDONE: Implement shape (experimental)\n\nIn order to use shape objects you need to call ```purgeWithShape(data, shape)``` from your server-side\ncode when returning data to client. If the function gets an undefined shape object it will return\nthe passed data AS IS. Note that the shape object passed here is a nested shape object.\n\nTo provide a more developer friendly syntax for shape objects you call ```createNestedShape(flatShape)``` which\nwill convert a flat list of expressions to a nested shape object you can pass to ```purgeWithShape()```.\n\nYou should always use the flat shape object syntax in clients for readability. Examples of the developer friendly flat shape object syntax can be seen here:\n\n```js\nshape = [\n  \"discountCode\",\n  \"firstRow\",\n  \"order\",\n  \"order.orderDate\",\n  \"order.orderCurrency\",\n  \"order.orderRows\",\n  \"order.orderRows.itemDescription\",\n  \"order.orderRows.sku\",\n  \"order.orderRows.qty\",\n]\n\n// All primitive props on first lever except email (skipping objects and arrays)\nshape = [\n  \"*\",\n  \"!email\",\n]\n\n// All props at first and all primitive types on second level\nshape = [\n  \"*.*\",\n]\n\n// All props on first two levels and primitive types on third level\nshape = [\n  \"*.*.*\",\n]\n\n// All primitive props at first level and all primitive types under order\nshape = [\n  \"*\",\n  \"order.*\",\n]\n\n// All primitive props at first level and all props for order, excluding the array orderRows\nshape = [\n  \"*\",\n  \"order.*.*\",\n  \"order.!orderRows\",\n]\n\n// All primitive props at first level and all props for order, limiting results in array orderRows\n// TODO: Implement limiting for arrays\nshape = [\n  \"*\",\n  \"order.*.*\",\n  \"order.orderRows[0-9]\",\n]\n```\n\nTODO: Create flat shape objects from an isomorphic-schema.\n\n```js\n// Create shape from provided schema (useful when getting data for forms etc.)\ncreateShapeFromSchema(schema)\n```\n\nExamples of nested shape objects:\n\n```js\n// Server-side filter of output is performed by Restish\n// Objects do a lookahead to next level to see if they should be included at all\npurgeWithShape(data, shape)\n[\n  [\"*\", \"!email\"],\n]\n[\n  [\"*\"],\n  [\"*.*\"],\n]\n[\n  [\"*\"],\n  [\"*.*\"],\n  [\"*.*.*\"],\n]\n[\n  [\"*\"],\n  [\"order.*\"],\n]\n[\n  [\"*\"],\n  [\"order.*\"],\n]\n[\n  [\"*\"],\n  [\"order.*\", \"order.!orderRows\"],\n  [\"order.*.*\"],\n]\n// Array options hasn't been implemented yest\n[\n  [\"*\"],\n  [\"order.*\", \"order.orderRows[0-9]\"],\n  [\"order.*.*\"],\n]\n```\n\n## VS Code\n\nThis is a launch.json snippet to run tests for debugging. Note setting the env var (see babel.config.js):\n\n```json\n    {\n      \"env\": {\n        \"TARGET\": \"server\"\n      },\n      \"args\": [\n        \"--require\",\n        \"@babel/register\",\n        \"-u\",\n        \"tdd\",\n        \"--timeout\",\n        \"999999\",\n        \"--colors\",\n        \"${file}\"\n      ],\n      \"internalConsoleOptions\": \"openOnSessionStart\",\n      \"name\": \"Run Mocha Test File\",\n      \"program\": \"${workspaceFolder}/node_modules/mocha/bin/_mocha\",\n      \"request\": \"launch\",\n      \"skipFiles\": [\n        \"\u003cnode_internals\u003e/**\"\n      ],\n      \"sourceMaps\": true,\n      \"type\": \"pwa-node\"\n    },\n```\n\nTODO: Did this break matching of routes when layered?\n  \"Removed unused options in matchPath, and specifying strict using exact\t2e8c9bc\tjhsware\"\n  \"/admin/:type\" without \"exact\" prop should match \"/admin/User/5f84669489e28231b5119220\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhsware%2Fkoa-restish","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjhsware%2Fkoa-restish","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjhsware%2Fkoa-restish/lists"}