{"id":13828959,"url":"https://github.com/prismyland/prismy","last_synced_at":"2025-07-09T07:30:48.036Z","repository":{"id":47505117,"uuid":"187440546","full_name":"prismyland/prismy","owner":"prismyland","description":":rainbow: Simple and fast type safe server library.","archived":false,"fork":false,"pushed_at":"2024-04-06T19:12:00.000Z","size":736,"stargazers_count":48,"open_issues_count":10,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-04-07T02:12:35.636Z","etag":null,"topics":["http","http-serv","micro","microservice","nodejs","nodejs-server","now","severless","typescript"],"latest_commit_sha":null,"homepage":"https://prismyland.github.io/prismy","language":"TypeScript","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/prismyland.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}},"created_at":"2019-05-19T06:09:41.000Z","updated_at":"2024-04-14T23:27:32.599Z","dependencies_parsed_at":"2024-04-14T23:27:22.400Z","dependency_job_id":"7b203549-93a8-439f-839b-2e3c1f814922","html_url":"https://github.com/prismyland/prismy","commit_stats":{"total_commits":219,"total_committers":6,"mean_commits":36.5,"dds":"0.31506849315068497","last_synced_commit":"d1b43b567c6b6cbec9922a15f3f4dc85176421b2"},"previous_names":[],"tags_count":53,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prismyland%2Fprismy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prismyland%2Fprismy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prismyland%2Fprismy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prismyland%2Fprismy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/prismyland","download_url":"https://codeload.github.com/prismyland/prismy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225492420,"owners_count":17482869,"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":["http","http-serv","micro","microservice","nodejs","nodejs-server","now","severless","typescript"],"created_at":"2024-08-04T09:03:22.707Z","updated_at":"2024-11-20T08:31:25.313Z","avatar_url":"https://github.com/prismyland.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cimg  width='240' src='resources/logo.svg' alt='prismy'\u003e\n\n# `prismy`\n\n:rainbow: Simple and fast type safe server library based on micro for now.sh v2.\n\n[![Build Status](https://travis-ci.com/prismyland/prismy.svg?branch=master)](https://travis-ci.com/prismyland/prismy)\n[![codecov](https://codecov.io/gh/prismyland/prismy/branch/master/graph/badge.svg)](https://codecov.io/gh/prismyland/prismy)\n[![NPM download](https://img.shields.io/npm/dm/prismy.svg)](https://www.npmjs.com/package/prismy)\n[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/prismyland/prismy.svg?logo=lgtm\u0026logoWidth=18)](https://lgtm.com/projects/g/prismyland/prismy/context:javascript)\n\n[Full API Documentation](https://prismyland.github.io/prismy/globals.html)\n\n## Index\n\n- [Getting Started](#getting-started)\n  - [Requisition](#requisition)\n  - [Installation](#installation)\n  - [Hello World](#hello-world)\n- [Guide](#guide)\n  - [Context](#context)\n  - [Selectors](#selectors)\n  - [Middleware](#middleware)\n  - [Session](#session)\n  - [Cookies](#cookies)\n  - [Routing](#routing)\n- [Example](#simple-example)\n- [Testing](#writing-tests)\n- [Gotchas](#gotchas-and-troubleshooting)\n\n## Concepts\n\n1. _Asynchronously_ pick required values of a handler from context(which having HTTP Request object: IncomingMessage).\n2. _Asynchronously_ execute the handler with the picked values.\n3. **PROFIT!!**\n\n## Features\n\n- Very small (No Expressjs, the only deps are micro and tslib)\n- Takes advantage of the asynchronous nature of Javascript with full support for async / await\n- Simple and easy argument injection for handlers (Inspired by ReselectJS)\n  - Completely **TYPE-SAFE**\n  - No more complicated classes / decorators, only simple functions\n  - Highly testable (Request handlers can be tested without mocking request or sending actual http requests)\n- Single pass (lambda) style composable middleware (Similar to Redux)\n\n## Getting Started\n\n### Requisition\n\n- TypeScript v4.x\n- Node v12.x and above\n\n### Installation\n\nCreate a package.json file.\n\n```sh\nnpm init\n```\n\nInstall prismy.\n\n```sh\nnpm install prismy --save\n```\n\nMake sure typescript strict setting is on if using typescript.\n\n`tsconfig.json`\n\n```json\n{\n  \"strict\": true\n}\n```\n\n### Hello World\n\n`handler.ts`\n\n```ts\nimport { prismy, res, Selector } from 'prismy'\n\nconst worldSelector: Selector\u003cstring\u003e = () =\u003e 'world'!\n\nexport default prismy([worldSelector], async world =\u003e {\n  return res(`Hello ${world}`) // Hello world!\n})\n```\n\nIf you are using now.sh or next.js you can just put handlers in the `pages` directory and your done!\nSimple, easy, no hassle.\n\nOtherwise, serve your application using node.js http server.\n\n`serve.ts`\n\n```ts\nimport handler from './handler'\nimport * as http from 'http'\n\nconst server = new http.Server(handler)\n\nserver.listen(process.env.PORT)\n```\n\nFor more in-depth application see the more in-depth [Example.](#simple-example)\n\n## Guide\n\n### Context\n\n`context` is a simple plain object containing native node.js's request instance, `IncomingMessage`.\n\n```ts\ninterface Context {\n  req: IncomingMessage\n}\n```\n\nContext is passed into all selectors and middlewares. It can be used to assist memoization and communicate between linked selectors and middlewares.\n\n:exclamation: **It is highly recommended to use `Symbol('property-name')` in order to prevent duplicating property names and end up overwriting something important.**\nRead more about Symbols [here.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)\n\nThis way of communicating via symbols on the context object is used in `prismy-session`.\n\n:exclamation: Due to how prismy resolves selectors, context should **NOT** be used to communicate between selectors. Due to its async nature resolution order cannot be guaranteed.\n\n### Selectors\n\nMany other server libraries support argument injection through the use of\ndecorators e.g InversifyJS, NestJS and TachiJS.\nDecorators can seem nice and clean but have several pitfalls.\n\n- Controllers must be declared as class. (But not class expressions)\n- Argument injection via decorators is not type-safe.\n\nAn example controller in NestJS:\n\n```ts\nfunction createController() {\n  class GeneratedController {\n    /**\n     * Using decorators in class expression is not allowed yet.\n     * So compiler will throw an error.\n     * https://github.com/microsoft/TypeScript/issues/7342\n     * */\n    run(\n      // Argument types must be declared carefully because Typescript cannot infer it.\n      @Query() query: QueryParams\n    ): string {\n      return 'Done!'\n    }\n  }\n  return GeneratedController\n}\n```\n\nPrismy however uses _Selectors_, a pattern inspired by ReselectJS.\nSelectors are simple functions used to generate the arguments for the handler. A Selector accepts a\nsingle `context` argument or type `Context`.\n\n```ts\nimport { prismy, res, Selector } from 'prismy'\n\n// This selector picks the current url off the request object\nconst urlSelector: Selector\u003cstring\u003e = context =\u003e {\n  const url = context.req.url\n  // So this selector always returns string.\n  return url != null ? url : ''\n}\n\nexport default prismy(\n  [urlSelector],\n  // Typescript can infer `url` argument type via the given selector tuple\n  // making it type safe without having to worry about verbose typings.\n  url =\u003e {\n    await doSomethingWithUrl(url)\n    return res('Done!')\n  }\n)\n```\n\nAsync selectors are also fully supported out of the box!\nIt will resolve all selectors right before executing handler.\n\n```ts\nimport { prismy, res, Selector } from 'prismy'\n\nconst asyncSelector: Selector\u003cstring\u003e = async context =\u003e {\n  const value = await readValueFromFileSystem()\n  return value\n}\n\nexport default prismy([asyncSelector], async value =\u003e {\n  await doSomething(value)\n  return res('Done!')\n})\n```\n\n#### Included Selectors\n\nPrismy includes some helper selectors for common actions.\nSome examples are:\n\n- `methodSelector`\n- `querySelector`\n\nOthers require configuration and so factory functions are exposed.\n\n- `createJsonBodySelector`\n- `createUrlEncodedBodySelector`\n\n```ts\nimport { createJsonBodySelector } from 'prismy'\n\n// createJsonBodySelector returns an AsyncSelector\u003cany\u003e\nconst jsonBodySelector = createJsonBodySelector({\n  limit: '1mb'\n})\n\nexport default prismy([jsonBodySelector], async jsonBody =\u003e {\n  await doSomething(jsonBody)\n  return res('Done!')\n})\n```\n\nThese helper selectors can be composed to provide more solid typing and error handling.\n\n```ts\nimport { createJsonBodySelector, Selector } from 'prismy'\n\ninterface RequestBody {\n  data: string\n  id?: number\n}\n\nconst jsonBodySelector = createJsonBodySelector()\n\nconst requestBodySelector: Selector\u003cRequestBody\u003e = context =\u003e {\n  const jsonBody = jsonBodySelector(context)\n  if (!jsonBody.hasOwnProperty('data')) {\n    throw new Error('Query is required!')\n  }\n  return jsonBody\n}\n\nexport default prismy([requestBodySelector], requestBody =\u003e {\n  return res(`You're query was ${requestBody.json}!`)\n})\n```\n\nFor other helper selectors, please refer to the [API Documentation.](#api)\n\n### Middleware\n\nMiddleware in Prismy works as a single pass pipeline of composed functions. The next middleware is\naccepted as an argument to the previous middleware allowing the request to be progressed or returned as desired.\nThe middleware stack is composed and so the response travels right to left across the array.\n\nThis pattern, much like Redux middleware, allows you to:\n\n- Do something before executing handler (e.g Session)\n- Do something after executing handler (e.g CORS, Session)\n- Do something other than executing handler (e.g Routing, Error handling)\n\n```ts\nimport { middleware, prismy, res, Selector, updateHeaders } from 'prismy'\n\nconst withCors = middleware([], next =\u003e async () =\u003e {\n  const resObject = await next()\n\n  return updateHeaders(resObject, {\n    'access-control-allow-origin': '*'\n  })\n})\n\n// Middleware also accepts selectors which can be used for DI and unit testing\nconst urlSelector: Selector\u003cstring\u003e = context =\u003e context.req.url!\nconst withErrorHandler = middleware([urlSelector], next =\u003e async url =\u003e {\n  try {\n    return await next()\n  } catch (error) {\n    return res(`Error from ${url}: ${error.message}`)\n  }\n})\n\nexport default prismy(\n  [],\n  () =\u003e {\n    throw new Error('Bang!')\n  },\n  /**\n   * The request will progress through the middleware stack like so:\n   * withErrorHandler =\u003e withCors =\u003e handler =\u003e withCors =\u003e withErrorHandler\n   * */\n  [withCors, withErrorHandler]\n)\n```\n\n### Session\n\nAlthough you can implement your own sessions using selectors and middleware, Prismy offers a\nsimple module to make it easy with `prismy-session`.\n\nInstall it using:\n\n```sh\nnpm install prismy-session --save\n```\n\n`prismy-session` exposes `createSession` which accepts a `SessionStrategy` instance and returns a\nselector and middleware to give to prismy.\nOfficial strategies include `prismy-session-strategy-jwt-cookie` and `prismy-session-strategy-signed-cookie`. Both available on npm.\n\n```ts\nimport { prismy, res } from 'prismy'\nimport createSession from 'prismy-session'\nimport JWTSessionStrategy from 'prismy-session-strategy'\n\nconst { sessionSelector, sessionMiddleware } = createSession(\n  new JWTSessionStrategy({\n    secret: 'RANDOM_HASH'\n  })\n)\n\ndefault export prismy(\n  [sessionSelector],\n  async session =\u003e {\n    const { data } = session\n    await doSomething(data)\n    return res('Done')\n  },\n  [sessionMiddleware]\n)\n\n```\n\n### Cookies\n\nPrismy also offers a selector for cookies in the `prismy-cookie` package.\n\n```ts\nimport { prismy, res } from 'prismy'\nimport { appendCookie, createCookiesSelector } from 'prismy-cookie'\n\nconst cookiesSelector = createCookiesSelector()\n\nexport default prismy([cookiesSelector], async cookies =\u003e {\n  /** appendCookie is a helper function that takes a response object and\n   * a string key, value tuple returning a new response object with the\n   * cookie appended.\n   */\n  return appendCookie(res('Cookie added!'), ['key', 'value'])\n})\n```\n\n### Routing\n\nFrom v3, `prismy` provides `router` method to create a routing handler.\n\n```ts\nimport { prismy, res } from 'prismy'\nimport { router } from 'prismy-method-router'\nimport http from 'http'\n\nconst myRouter = router([\n  [\n    ['/posts', 'get'], prismy([], () =\u003e {\n      const posts = fetchPostList()\n\n      return res({ posts })\n    })\n  ],\n  [\n    ['/posts', 'post'], prismy([bodySelector], (body) =\u003e {\n      const post = createPost(body)\n\n      return redirect(`/posts/${post.id}`)\n    })\n  ],\n  [\n    // GET method can be omitted\n    // You can select route param with `createRouteParamSelector`\n    '/posts/:postId', prismy([createRouteParamSelector('postId')], (postId) =\u003e {\n      const post = fetchOnePost(postId)\n\n      return res({ post })\n    })\n  ]\n])\n\n// Router is a prismy handler. You can directly pass to the server\nconst server = new http.Server(handler)\n\nserver.listen(process.env.PORT)\n```\n\n\u003e Routing handler is using path-to-regexp internally. Please check their document to learn more routing behavior.\nhttps://github.com/pillarjs/path-to-regexp\n\n## Simple Example\n\n```ts\nimport {\n  createJsonBodySelector,\n  middleware,\n  prismy,\n  querySelector,\n  redirect,\n  res,\n  Selector\n} from 'prismy'\nimport { methodRouter } from 'prismy-method-router'\nimport createSession from 'prismy-session'\nimport JWTSessionStrategy from 'prismy-session-strategy-jwt-cookie'\n\nconst jsonBodySelector = createJsonBodySelector({\n  limit: '1mb'\n})\n\nconst { sessionSelector, sessionMiddleware } = createSession(\n  new JWTSessionStrategy({\n    secret: 'RANDOM_HASH'\n  })\n)\n\nconst authSelector: Selector\u003cUser\u003e = async context =\u003e {\n  const { data } = await sessionSelector(context)\n  const user = await getUser(data.user_id)\n  return user\n}\n\nconst authMiddleware = middleware([authSelector], next =\u003e async user =\u003e {\n  if (!isAuthorized(user)) {\n    return redirect('/login')\n  }\n  return next()\n})\n\nconst todoIdSelector: Selector\u003cstring\u003e = async context =\u003e {\n  const query = await querySelector(context)\n  const { id } = query\n  if (id == null) {\n    throw new Error('Id is required!')\n  }\n  return Array.isArray(id) ? id[0] : id\n}\n\nconst contentSelector: Selector\u003cstring\u003e = async context =\u003e {\n  const jsonBody = await jsonBodySelector(context)\n  const { content } = jsonBody\n  if (content == null) {\n    throw new Error('content is required!')\n  }\n  return jsonBody.content\n}\n\nexport default methodRouter(\n  {\n    get: prismy([], async () =\u003e {\n      const todos = await getTodos()\n      return res({ todos })\n    }),\n    post: prismy([contentSelector], async content =\u003e {\n      const todo = await createTodo(content)\n      return res({ todo })\n    }),\n    delete: prismy([todoIdSelector], async id =\u003e {\n      await deleteTodo(id)\n      return res('Deleted')\n    })\n  },\n  [authMiddleware, sessionMiddleware]\n)\n```\n\n## Writing Tests\n\nPrismy is designed to be easily testable. To furthur ease testing `prismy-test` exposes the `testHandler` function to create quick and easy end to end tests.\n\n### E2E Tests\n\nEnd to end tests are very simple.\n\n```ts\nimport got from 'got'\nimport { testHandler } from \"prismy-test\"\nimport handler from './handler'\n\ndescribe('handler', () =\u003e {\n  it('e2e test', async () =\u003e {\n    await testHandler(handler, async url =\u003e {\n      const response = await got(url, {\n        method: 'POST',\n        responseType: 'json',\n        json: {\n          ... // JSON data\n        }\n      })\n      expect(response).toMatchObject({\n        statusCode: 200,\n        body: '/'\n      })\n    })\n  })\n})\n```\n\n### Unit Tests\n\nThanks to Prismy's simple, function-based architecture unit testing in Prismy is extremely simple.\nPrismy handler exposes its original handler function so you can directly unit test the handler function even if it is an anonymous function argument to `prismy` without needing to mock http requests.\n\n```ts\nimport handler from './handler'\n\ndecribe('handler', () =\u003e {\n  it('unit test', () =\u003e {\n    /**\n     * Access the original handler function\n     * */\n    const result = handler.handler({\n      ... // whatever arguments you want to test with\n    })\n\n    expect(result).toEqual({\n      body: 'Done!',\n      headers: {},\n      statusCode: 200\n    })\n  })\n})\n```\n\n## Gotchas and Troubleshooting\n\n### Long `type is not assignable to [Selector\u003cunknown\u003e ...` error when creating Prismy handler\n\n- Selectors must be written directly into the array argument in the function call. This is due to a limitation of Typescript type inference. Prismy relies on knowning the tuple type of the array, e.g `[string, number]`. Dynamicly creating the array will infer as `string|number[]` which means Prismy cannot infer the positional types for the handler arguments.\n\n```ts\nconst selectors = [selector1, selector2]\nprismy(selectors, handler) // will give type error\n\nprismy([selector1, selector2], handler) // Ok!\n```\n\n- This weird type error may also occur if the handler does not return a `ResponseObject`. Use `res(..)` to generate a `ResponseObject` easily.\n\n```ts\n// Will show crazy error.\nprismy([selector1, selector2], (one, two) =\u003e {\n  return 'Not a ResponseObject'\n})\n\n// Ok!\nprismy([selector1, selector2], (one, two) =\u003e {\n  return res('Is a ResponseObject')\n})\n```\n\n### Long `type is not assignable to [Selector\u003cunknown\u003e ...` error when creating middleware\n\n- mhandler argument must be of `type next =\u003e async () =\u003e T`. Remember the async.\n- If using Typescript, `'strict'` compiler option MUST be `true`. This can be set in tsconfig.json.\n\n\u003c!-- TODO: add api docs --\u003e\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprismyland%2Fprismy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprismyland%2Fprismy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprismyland%2Fprismy/lists"}