{"id":19419067,"url":"https://github.com/launchcodedev/router","last_synced_at":"2025-04-24T14:31:39.427Z","repository":{"id":39660158,"uuid":"275402712","full_name":"launchcodedev/router","owner":"launchcodedev","description":"A wrapper for Koa route actions w/ schemas, middleware and more","archived":false,"fork":false,"pushed_at":"2023-01-24T03:14:55.000Z","size":2649,"stargazers_count":6,"open_issues_count":13,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-19T12:09:40.992Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/launchcodedev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-27T15:42:45.000Z","updated_at":"2021-06-24T20:43:26.000Z","dependencies_parsed_at":"2023-02-13T14:16:16.764Z","dependency_job_id":null,"html_url":"https://github.com/launchcodedev/router","commit_stats":null,"previous_names":[],"tags_count":60,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchcodedev%2Frouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchcodedev%2Frouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchcodedev%2Frouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchcodedev%2Frouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/launchcodedev","download_url":"https://codeload.github.com/launchcodedev/router/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250643410,"owners_count":21464169,"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":[],"created_at":"2024-11-10T13:16:03.448Z","updated_at":"2025-04-24T14:31:39.126Z","avatar_url":"https://github.com/launchcodedev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Launchcode Router\n[![Licensed under MPL 2.0](https://img.shields.io/badge/license-MPL_2.0-green.svg)](https://www.mozilla.org/en-US/MPL/2.0/)\n[![Build Status](https://github.com/launchcodedev/router/workflows/CI/badge.svg)](https://github.com/launchcodedev/router/actions)\n[![npm](https://img.shields.io/npm/v/@lcdev/router.svg)](https://www.npmjs.com/package/@lcdev/router)\n\nThis is our main `@lcdev/router` node package, for centralizing the logic that\nall of our backend applications share. It's designed for usage in koa servers.\n\n```bash\nyarn add @lcdev/router@VERSION\n```\n\nIt's built fairly simply, with a couple core ideas:\n\n- Routes are contained within one folder, with a flat structure\n- Routes are hierarchical, but usually one level deep\n- Routes typically consist of one \"action\" (where business logic lives), preceded by middleware\n\nTo help development remain consistent, we've made a package for encapsulating that logic.\nThis is not a web server or framework - it's a wrapper for the organic structure of our backends.\n\nSo how do you use it?\n\n```typescript\nimport { join } from 'path';\nimport { createRouter } from '@lcdev/router';\n\n// here, we load files from a folder (./routes) that contains many routers\n// `api` here conglomerates all of them into one single koa router\nconst api = await createRouter(join(__dirname, 'routes'));\n\n// you can use the router just like koa-router\nconst myServer = new Koa();\n\nmyServer\n  .use(api.routes())\n  .use(api.allowedMethods());\n```\n\nWhat about those files in `./routes`? Let's look at their expected structure.\n\nBelow is one of the files in the `routes` folder:\n\n```typescript\nimport {\n  RouteFactory,\n  RouteActionWithContext,\n  HttpMethod,\n  route,\n  bindRouteActions,\n} from '@lcdev/router';\n\n// we'll leave this blank for now\ntype Dependencies = {};\n\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  getDependencies() {\n    // here, we return whatever Dependencies is\n    return {};\n  },\n\n  create(dependencies) {\n    // here, bindRouteActions adds `dependencies` as `this` in actions\n    // it's not required (returning an array is fine), but it makes things easier\n    return bindRouteActions(dependencies, [\n      // here, we'll wrap one of the route definitions in the `route` function\n      // route is optional (an object works), but it adds better type inference\n      route({\n        path: '/hello-world',\n        method: HttpMethod.GET,\n        async action(ctx) {\n          // returning here is the same as setting `ctx.body`\n          return { hello: 'world!' };\n        },\n      }),\n    ]);\n  },\n};\n\n// important - default export needs to be a RouteFactory or a class implementing it\nexport default factory;\n```\n\nAlright, so we now have a 'RouteFactory', which can be consumed by our router.\nIf this file was in `./routes`, you'd now have a successful `/hello-world` GET route.\n\nA few explanations:\n\n1. We export a RouteFactory to make the router side-effect free (you can import it without requiring everything to be initialized)\n2. We define `Dependencies` so that you can be explicit about what other modules are used, usually this is a database connection or other integration\n\nSo on to dependencies:\n\n```typescript\nimport {\n  RouteFactory,\n  RouteActionWithContext,\n  HttpMethod,\n  route,\n  bindRouteActions,\n} from '@lcdev/router';\n\ntype Dependencies = {\n  // normally, you'd be a bit more concise and call this `kx` or `cx`\n  databaseConnection: Postgres;\n};\n\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  getDependencies() {\n    return {\n      // we establish the database connection now\n      // this avoids the need to have it ready until actually using this router\n      databaseConnection: getTheDefaultDatabaseConnection(),\n    };\n  },\n\n  create(dependencies) {\n    return bindRouteActions(dependencies, [\n      route({\n        path: '/some-entity',\n        method: HttpMethod.GET,\n        async action(ctx) {\n          // we now have access to `databaseConnection` through `this`!\n\n          // and we can return whatever we want, which will end up as a json response!\n          return this.databaseConnection.query('select * from some_entity');\n        },\n      }),\n    ]);\n  },\n};\n```\n\nThe key here is, that `getDependencies` is solely a helper. For testing, you might forgo it entirely,\nand `create` the router yourself with a mocked up database.\n\n### Testing\nBefore we go too deep, check out the [testing](https://www.npmjs.com/package/@lcdev/router-testing) package.\nIt provides a very simple way to use these route factories as test fixtures.\n\n### Prefix\nPrefixes get applied to all actions in a router. That means `prefix: '/auth'` puts all your actions within\nthat path prefix. You can forgo this and specify absolute paths in your actions if you want.\n\n```typescript\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  prefix: '/auth',\n\n  getDependencies() { ... },\n  create(dependencies) { ... },\n};\n```\n\n### Middleware\nYou can declare middleware for a router, and/or per route. This allows flexibility and coverage.\n\n```typescript\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  // your normal getDependencies and create functions\n  getDependencies() { ... },\n  create(dependencies) { ... },\n\n  middleware: (dependencies) =\u003e [\n    // middleware here gets applied to all actions\n    // you might put authentication middleware here, for example\n  ],\n};\n```\n\nThe same interface is available per-action. Just specify `middleware: []` beside `path` and friends.\n\n### Schemas\nWe support JSON Schema natively to validate incoming request bodies. Simply put a `schema` property next\nto your `action`.\n\n```typescript\nroute({\n  path: '/resource/:id',\n  method: HttpMethod.POST,\n  // we give you @serafin/schema-builder through the `emptySchema` export\n  // you can also using a json schema directly, using the `JSONSchema` export\n  schema: emptySchema()\n    .addNumber('x')\n    .addNumber('y'),\n  async action(_, body) {\n    // here, typescript will actually know the type of x and y!\n    const { x, y } = body;\n  },\n})\n```\n\nThis does depend on having `bodyparser` middleware. We export `bodyparser`, for common use cases, from this module.\nIt's good practice to include this bodyparser per-route-factory.\n\n```typescript\nimport { RouteFactory, bodyparser, propagateErrors, propagateValues } from '@lcdev/router';\n\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  getDependencies() { ... },\n\n  // a normal normal middleware stack looks like this\n  middleware: ({ auth }) =\u003e [\n    propagateErrors(true),\n    propagateValues(),\n    bodyparser(),\n    auth.authenticate(),\n  ],\n\n  create(dependencies) { ... },\n};\n```\n\n### Nesting\nThe lcdev router is usually used in mostly flat contexts, but you can easily nest your routers.\n\n```typescript\nimport {\n  RouteFactory,\n  findRoutes,\n} from '@lcdev/router';\n\nconst factory: RouteFactory\u003cDependencies\u003e = {\n  prefix: '/support',\n  nested: () =\u003e findRoutes(join(__dirname, 'support')),\n\n  getDependencies() { ... },\n  create(dependencies) { ... },\n};\n```\n\nThe example above nests routes found in the `./support` folder.\n\n### Errors\nThe lcdev router normalizes errors that come from your actions. This pairs nicely with `@lcdev/logger`.\n\nWhat you need to know:\n\n- `@lcdev/router` exports `BaseError` (you can use `err`), which is \"a user visible error\"\n- In development, you'll always see your error messages\n- In production, only errors that are BaseErrors propagate up (see `internalMessage` for full details)\n\n**Throwing errors**: it happens, you'll need a way to throw an error up when things go wrong.\n\n```typescript\nimport { err } from '@lcdev/router';\n\n// is it okay for your API consumers to see this error?\nthrow err(401, 'Your error message');\n\n// no? keep it private by throwing any other error type\nthrow { status: 401, message: 'Your error message' };\n```\n\nYou'll likely want to use `propagateErrors`, though it is, strictly speaking, optional.\n\n```typescript\nimport { propagateErrors } from '@lcdev/router';\n\n// try to keep this as high as you can in your middleware stack\nmyServer.use(propagateErrors());\n\nmyServer.use(api.routes());\nmyServer.use(api.allowedMethods());\n```\n\nThis will catch normalized errors, and return them in our standard json body format (and set the HTTP code).\n\n```json\n{\n  \"success\": false,\n  \"code\": \"ERRCODE|num\",\n  \"message\": \"User visible message\"\n}\n```\n\nYou're encouraged to add this middleware at the top of your app, as well as on every RouteFactory. Doing so\nper-factory will make testing those factories in isolation a lot easier.\n\n### Return Format\nIn a similar way to errors, it's handy to have all of your routes return JSON in the same structured format.\n\n```json\n{\n  \"success\": true,\n  \"data\": { ... }\n}\n```\n\nInstead of doing this yourself, we have middleware to help. Again, this is optional but encouraged.\n\n```typescript\nimport { propagateValues } from '@lcdev/router';\n\nmyServer.use(propagateValues());\n```\n\nWhen this middleware is above your route actions, you don't need to do anything. JSON responses will be wrapped\nin the above format. This makes parsing your API responses a lot easier.\n\nBy default, this supports a third \"meta\" property in return objects. We normally use this for pagination state.\nYou can add data the `ctx.state.meta` or call `addMeta(ctx, { ... })` to fill this in in an action.\n\n### Pagination\nActions will look basically like this:\n\n```typescript\nimport { paginate, addPagination, paginationSchema, Pagination } from '@lcdev/router';\n\nroute({\n  path: '/',\n  method: HttpMethod.GET,\n  middleware: [\n    // wrapping middleware around your action\n    // verifies 'page: number' and 'count?: number' query parameters\n    // 100 is the default count/pageSize when not provided\n    paginate(100), // second (optional) parameter here is the maximum limit allowed\n  ],\n  querySchema: paginationSchema,\n  returning: [getApiFields(MyEntity)],\n  async action(ctx) {\n    // this is populated by the paginate middleware\n    const { page, pageSize } = ctx.state.pagination as Pagination;\n\n    // just use page and pageSize as you normally would\n    const { results, total } = await MyEntity.query(this.kx)\n      .page(page, pageSize);\n\n    // pushes the resulting total into ctx state\n    addPagination(ctx, total);\n\n    // after this, 'total' and 'pages' are added as meta properties to the response\n    return results;\n  },\n}),\n```\n\n### Extracting Returns\nTaking an example route action:\n\n```typescript\nroute({\n  path: '/users',\n  method: HttpMethod.GET,\n  async action(ctx) {\n    return myDatabase.select('* from user');\n  },\n}),\n```\n\nYou might prefer not to include the `password` field here (excuse the contrived example).\n\nTo do so, the manual approach is:\n\n```typescript\nconst { values, to, return } = { ... };\n\nreturn { values, to, return };\n```\n\nThis is clearly not great. Lots of duplication and possibility for errors. It doesn't work\nfor nesting objects well, and with multiple branches in an action, requires duplication.\n\nYou might opt to use our `returning` field instead.\n\n```typescript\nroute({\n  path: '/users',\n  method: HttpMethod.GET,\n  returning: {\n    firstName: true,\n    lastName: true,\n    permissions: [{\n      role: true,\n      authority: ['access'],\n    }],\n  },\n  async action(ctx) {\n    return myDatabase.select('* from user');\n  },\n}),\n```\n\nYou can think of this as the inverse of `schema`. Some examples of this:\n\n```\nINPUT:\n{\n  firstName: 'Bob',\n  lastName: 'Albert',\n  password: 'secure!',\n  permissions: [\n    { role: 'admin', timestamp: new Date(), authority: { access: 33 } },\n    { role: 'user', timestamp: new Date(), extra: false },\n  ],\n}\n\nRETURNING:\n{\n  firstName: true,\n  lastName: true,\n  permissions: [{\n    role: true,\n    authority: ['access'],\n  }],\n}\n\nRESULT:\n{\n  firstName: 'Bob',\n  lastName: 'Albert',\n  permissions: [\n    { role: 'admin', authority: { access: 33 } },\n    { role: 'user' },\n  ],\n}\n```\n\nNote a couple things:\n\n- `['access']` means \"pull these fields from the object\" - it's the same as `{ access: true }`\n- `[{ ... }]` means \"map this array with this selector\"\n- `{ foo: true }` means \"take only 'foo'\"\n\nMismatching types, like an array selector when the return is an object, are ignored.\n\nThis is pulled directly from the [`@lcdev/mapper`](../utilities/mapper.html) package, you can read more there.\n\n### API Fields\nYou might want to reduce the duplication when using the `returning` feature. Most of the time,\nyou want to return the same fields for the same entities, records, etc.\n\nPlease see the [`@lcdev/api-fields`](https://github.com/launchcodedev/api-fields) package for that. It defines a decorator, called `@ApiField()`,\nwhich you can use to automatically fill in the `returning` field of a route action.\n\n```typescript\nimport { ApiField } from '@lcdev/api-fields';\n\nclass User extends BaseEntity {\n  @ApiField()\n  id: number;\n\n  privateField: number;\n\n  @ApiField()\n  firstName: string;\n\n  @ApiField(() =\u003e Permission)\n  permission: Permission;\n\n  ...\n}\n```\n\nIn your route action, simply:\n\n```typescript\nimport { getApiFields } from '@lcdev/api-fields';\n\nroute({\n  path: '/users/:id',\n  method: HttpMethod.GET,\n  // getApiFields returns an object with the same format that `returning` expects\n  returning: getApiFields(User),\n  async action(ctx) {\n    return myDatabase.select('from user where id = $0', id).first();\n  },\n}),\nroute({\n  path: '/users',\n  method: HttpMethod.GET,\n  // can be composed easily - an array of users is just like this\n  returning: [getApiFields(User)],\n  async action(ctx) {\n    return myDatabase.select('from user where id = $0', id);\n  },\n}),\n```\n\nPlease read more on the [docs](https://github.com/launchcodedev/api-fields).\n\n### Incremental Adoption\nFor apps that are using a basic `koa-router` and don't want the module loading\nof this package, this enables you to still use route (giving you schema validation,\nmiddleware, error handling, api fields / returning extraction).\n\n```typescript\nimport * as Router from 'koa-router';\nimport { route, addRouteToRouter, addRoutesToRouter, HttpMethod } from '@lcdev/router';\n\nconst router = new Router();\n\naddRouteToRouter(\n  route({\n    path: '/my-route',\n    method: HttpMethod.GET,\n    returning: {\n      foo: true,\n    },\n    async action() {\n      return {\n        foo: 'bar',\n        bar: 'baz',\n      };\n    },\n  }),\n  router,\n);\n\n// or\n\naddRoutesToRouter(router, [\n  route({\n    path: '/my-route',\n    method: HttpMethod.GET,\n    async action() {\n      return {\n        foo: 'bar',\n        bar: 'baz',\n      };\n    },\n  }),\n]);\n\nexport default router;\n```\n\nThis enable incremental adoption, though without the benefit of DI, nesting, etc.\n\n### Open API\nThere is early support for generating API documentation from your routers. Check out the `createOpenAPI`\nfunction for more about this. For now, we encourage you to use Insomnia for testing of APIs.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchcodedev%2Frouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaunchcodedev%2Frouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchcodedev%2Frouter/lists"}