{"id":19994821,"url":"https://github.com/kyr0/kiss-arch","last_synced_at":"2026-06-14T07:31:18.377Z","repository":{"id":143882525,"uuid":"469374649","full_name":"kyr0/kiss-arch","owner":"kyr0","description":"Web App architecture kept simple and stupid (TypeScript, JS)","archived":false,"fork":false,"pushed_at":"2022-03-17T09:42:14.000Z","size":68,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-28T19:43:52.158Z","etag":null,"topics":["app","architecture","bus","cqrs","global","i18next","javascript","store","translation","typescript-library","webapp"],"latest_commit_sha":null,"homepage":"","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/kyr0.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}},"created_at":"2022-03-13T13:08:56.000Z","updated_at":"2022-05-02T10:39:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"97e9f701-ff2b-496f-8f3f-1f111b4d4078","html_url":"https://github.com/kyr0/kiss-arch","commit_stats":{"total_commits":7,"total_committers":1,"mean_commits":7.0,"dds":0.0,"last_synced_commit":"d82ef1679872f9a6e2a0ba45a6d5f441e0968564"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kyr0/kiss-arch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyr0%2Fkiss-arch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyr0%2Fkiss-arch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyr0%2Fkiss-arch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyr0%2Fkiss-arch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kyr0","download_url":"https://codeload.github.com/kyr0/kiss-arch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kyr0%2Fkiss-arch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34313515,"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-14T02:00:07.365Z","response_time":62,"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":["app","architecture","bus","cqrs","global","i18next","javascript","store","translation","typescript-library","webapp"],"created_at":"2024-11-13T04:57:44.882Z","updated_at":"2026-06-14T07:31:18.349Z","avatar_url":"https://github.com/kyr0.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kiss-arch\n\nHaving an architecture in web apps is crucial to keep the cyclic complexity low.\nIn software development, there is one simple rule: Complexity is the devil.\nHowever, the web is full of opinions, ideas, patterns and concepts.\n\nOver time, simple ideas tend to grow into extrodinary complex systems,\nand what once has been the purpose - to make things easier - often becomes\na nightmare.\n\nThis libary implements architecture patterns with the beloved\n\"keep it simple, stupid\" philosophy in mind.\n\n## Setup\n\n    yarn add kiss-arch\n    npm i kiss-arch\n\nFull library size, no terser: `~4kb` (`~1.5kb` gzipped, respectively).\nFootprint might be lower if you're using a tree-shaking enabled bundler.\n\n## Usage\n\n### Global Variables; Typed Global Caching\n\nSometimes you need to store values globally, but in Node.js, Deno and\nthe Browser, we've a different \"global\" scope.\n\nHowever there is `globalThis`, but we don't want untyped globals, and\nwe don't want clashing of names on in global scope.\n\n```ts\n// in global.ts, which needs to be loaded in App.ts(x)\nimport { setGlobal, getGlobal } from 'kiss-arch'\n\nexport interface FooBar {\n  bar: number\n}\n\n// you're well advised to prefix any global variable\n// e.g. never use 'name', 'window', etc.\nexport const CACHE_FOO_NAME = 'myAppName_foo'\n\n// somewhere, e.g.a fetch() request loaded FooBar data from an HTTP endpoint\n// now we can cache it across files, scopes etc. easily\nsetGlobal\u003cFooBar\u003e(CACHE_FOO_NAME, { bar: 123 })\n\n// somewhere else, e.g. in another file, read the data from the global cache\nconst fooBar = getGlobal\u003cFooBar\u003e(CACHE_FOO_NAME)\n```\n\n### App mode\n\nEvery app is developed on developers machines, but finally they should\nrun on an arbitrary other environment like `staging` or `test`, and finally in `production`.\n\nSo, it might be a beneficial when application behaviour might differ between them,\nbut please, only for debugging/tracing purposes, not for general application logic,\notherwise you'll face bugs in production that weren't able to be discovered in\ndevelopment/test.\n\nBut every runtime environment, such as Browser, Node.js, Deno etc., and often even\nper framework/bundler tooling etc. the original value set for the mode might differ.\nIf we want to re-use code or just be flexible, we need to abstract that.\n\nThis is how we do it:\n\n```ts\n// in mode.ts, which needs to be loaded in App.ts(x)\nimport { getMode, setMode } from 'kiss-arch'\n\n// entrypoint of your application\n// there is only 'development' or 'production' mode\nsetMode('development')\n\n// whereever you're in your application check typed, e.g.\nif (getMode() === 'development') {\n  console.log('your debugging/tracing code goes here...')\n}\n```\n\n### Nano Store(s)\n\nIn application development, we always have to deal with data (storage, persistency),\nand logic (algorithms, decision making).\n\nNow data needs to be modelled well. It's desiable to use domain driven modelling for this.\nDepending on the requirements, you might night one or more \"storage places\", like shelfs.\nYou might want to define one per purpose which only holds data for a certain kind, like shoes, or food - you wouldn't place stinky shoes next to fresh salat, wouldn't you?!\n\nTherefore, stores should be typed. They should be able to `set`, `get` single entries,\ncheck if the store `has` an entry, being able to `remove` entries, and also be able\nto `persist` and `load` data, temporary or for long-term, depending on the requirements.\n\nDoes this need to be so complicated?\nActually, it can be very simple, but we want to use some advanced typing\nand domain modelling so that we're always on the safe side when working with data:\n\n```ts\n// appStore.ts, which needs to be loaded in App.ts(x)\nimport { getStore } from 'kiss-arch'\n\n// define the appState (a global application state)\nexport interface AppState {\n  isSettingsDialogOpen: boolean\n  userName: string\n}\n\n// the whole store interface may consist of many sub-state objects\nexport interface AppStore {\n  // appState is a nano state, a subset of the whole store\n  appState: AppState\n\n  // another typical use-case would be an applications Feature Flags.\n  // featureFlags: FeatureFlags\n}\n\n// every application needs initial values, defaults\nexport const DEFAULTS_APP_STATE: AppState = {\n  isSettingsDialogOpen: false, // don't open by default, e.g. first app open\n  userName: null, // we don't know the user on first app open\n}\n\n// we need a global cache name\nexport const APP_STORE_IDENT_NAME = '_APP_STORE'\n\n// also the key name of the sub-state should be defined\nexport const APP_STATE_PROP_NAME = 'appState'\n\n// we get an instance of the store via its global cache name\nexport const appStore = getStore\u003cAppStore\u003e(APP_STORE_IDENT_NAME)\n\n// we load the nano sub-set (state might have been saved before)\n// we also need to reference the default values, if not\nappStore.load(APP_STATE_PROP_NAME, DEFAULTS_APP_STATE)\n\n// optionally, we define some helper functions for storing sub-state\n// save() uses LocalStorage, saveForSession() would use SessionStorage\n// if those interfaces are not available, a mocked interface is used\n// (backed by a global variable)\nexport const saveAppState = () =\u003e appStore.save(APP_STATE_PROP_NAME)\nexport const getAppState = (): AppState =\u003e appStore.get(APP_STATE_PROP_NAME) || DEFAULTS_APP_STATE\n\n// overloading of types makes sure that if the developer\n// sets a nano subset key (e.g. 'isSettingsDialogOpen')\n// only the correct value type can be assinged\nexport const setAppState: Overloading\u003cAppState, keyof AppState\u003e = (\n  key: keyof AppState,\n  value: AppState[keyof AppState],\n) =\u003e {\n  getAppState()[key] = value as never\n\n  // in this application, setting a state would always save to LocalStorage\n  // so that when the window is reloaded, state is restored (see appStore.load() above)\n  saveAppState()\n}\n```\n\nAnd this is, how we can use this apps nano store:\n\n```ts\n// e.g. in some handler function that handles dialog opening\nsetAppState('isSettingsDialogOpen', true)\n\n// e.g. fetch nano app substate; it has full typing support\ngetAppState().isSettingsDialogOpen\n```\n\n## Event Bus\n\nWiring application logic can become a tedious task. Once many operations need\nto be triggered because of one single reason, the typical solution is to hard-wire calls.\nHowever, this leads to a lot of hard code dependencies and might end up in\nso called \"spaghetti code\" where one call follows another, and an application\nends up to be a huge chain of conditional function calls.\n\nUsing an event bus is a neat way to solve this, but event busses are often\nthought of the be hard to use and/or implement.\n\nThis mustn't be true. Only use the raw event bus if requirements make it\ndesirable to react on input events (cause) with more than one handler\nfunctions (effect), and if this should never end (=\u003e ergo, a \"stream of events\").\n\n```ts\n// appEvents.ts, which needs to be loaded in App.ts(x)\nimport { getBus } from 'kiss-arch'\n\n// define some event object to be send over the bus\n// this usually carries information, like function arguments would\nexport interface SendPushNotificationPayload {\n  message: string\n  icon: string\n}\n\n// we need some event name\nexport const EVENT_EVENT_SEND_PUSH_NOTIFICATIOON = 'sendPushNotification'\n\n// get a bus instance to broadcast events of a specific kind (cause)\nexport const notificationsBus = getBus\u003cEVENT_LOGIN, SendPushNotificationPayload\u003e('notificationsBus')\n\n// somewhere else, you need to register a handler that will be\n// called for login request (effect, triggered by .emit(...))\nnotificationsBus.on(EVENT_EVENT_SEND_PUSH_NOTIFICATIOON, async (payload: SendPushNotificationPayload) =\u003e {\n  // e.g. trigger FCM (Firebase Cloud Messaging)\n})\n\n// somebody logs-in via button tap,\n// but also when someone logs-in via some other UI (trigge the cause)\nnotificationsBus.emit(EVENT_EVENT_SEND_PUSH_NOTIFICATIOON, {\n  message: 'You have achieved a new highscore!',\n  icon: 'goal',\n})\n```\n\n## CQRS / Command Query Request Segregation\n\nWe've seen the event bus - it is capable of emitting and handling events and their payload\nvia the publish/subscribe messaging pattern. However, the event bus is designed to\nhandle infinite streaming messaging. But more often than that, we want to implement\nthe request and response messaging pattern where a unique request needs to be answered\ndirectly with a unique answer.\n\nNow we could use the event bus for that and alwas reply with another event once we received and handled one. However, this is tedious, and can be abstracted.\n\nWe understand triggering events as `commands` or `queries`, a command is handled\nwith an action handler that actually does something. A quers is handled with a\nquery handler that returns some data. Technically, both are implemented in the same way, but for application architecture, it is important to seperate the concerns:\n\n```ts\n// appCommands.ts, which needs to be loaded in App.ts(x)\nimport { addCommandResponseHandler, command, CommandActor, CommandHandler } from 'kiss-arch'\n\nexport interface PayloadOpenClose {\n  open: boolean\n}\n\nexport interface PayloadLogin {\n  username: string\n  password: string\n}\n\nexport interface PayloadLoginResponse {\n  isValid: boolean\n  message: string\n}\n\nexport type AppCommandName = 'toggleSettingsDialog' | 'login'\n\nexport const appCommand = async \u003cCommandPayload, CommandResponsePayload = unknown\u003e(\n  commandName: AppCommandName,\n  payload: CommandPayload,\n  oneTimeResponseHandler?: CommandHandler\u003cCommandResponsePayload\u003e,\n) =\u003e command(commandName, payload, oneTimeResponseHandler)\n\nexport const appCommandHandler = \u003cCommandPayload, CommandResponsePayload\u003e(\n  commandName: AppCommandName,\n  actor: CommandActor\u003cCommandPayload, CommandResponsePayload\u003e,\n) =\u003e {\n  addCommandResponseHandler\u003cAppCommandName, CommandPayload, CommandResponsePayload\u003e(commandName, actor)\n}\n```\n\nAnd this is how we use the above abstraction:\nFirst we define handlers. It is important to load this code via `import` early.\n\n```ts\n// e.g. commands/loginHandler.ts which needs to be loaded in appCommands.ts\nimport { appCommandHandler, PayloadLogin, PayloadLoginResponse } from '../appCommands'\n\nexport const COMMAND_LOGIN = 'login'\n\nappCommandHandler(COMMAND_LOGIN, async (payload: PayloadLogin) =\u003e {\n  // e.g. login against a HTTP API\n  // const loginResponse = await (await fetch(`https://foo.bar/login`, { 'Authorization': `Basic ${payload.username}+${payload.password}`})).json()\n\n  return {\n    isValid: loginResponse.success,\n    message: loginResponse.message || 'Login successful',\n  } as PayloadLoginResponse\n})\n```\n\nNow from whereever in the app, e.g. a login button, we can run the command:\n\n```ts\nappCommand\u003cPayloadLogin, PayloadLoginResponse\u003e(\n  COMMAND_LOGIN,\n  {\n    password: 'foo',\n    username: 'bar',\n  },\n  async (loginResponse: PayloadLoginResponse) =\u003e {\n    // here, we're directly receiving the answer \"in-place\"\n    console.log('PayloadLoginResponse', loginResponse.isValid, loginResponse.message)\n  },\n)\n```\n\n## i18n / translation\n\nOne of the common features of an App is to be translatable to the users language.\nThis, however, is not always the most simple task. You probably need some advanced\nfeatures such as: Variable interpolation, splitting of translation messages per module,\nand formatting functions.\n\nFirst we load our translations:\n\n```ts\n// in i18n.ts, which needs to be loaded in App.ts(x)\nimport { setTranslations } from 'kiss-arch'\n\n// import JSON files directly, you can also use JSON5 with an external module, if desired\nimport de from 'i18n/de.json'\nimport en from 'i18n/en.json'\n\nsetTranslations('en', en)\nsetTranslations('de', de)\n```\n\nA translation file could look like that, e.g. for german:\n\n```json\n{\n  \"Hello world\": \"Hallo Welt\",\n  \"Hello \u003cb\u003eWorld\u003c/b\u003e\": \"Hallo \u003cb\u003eWelt\u003c/b\u003e\",\n  \"Hello world {name}\": \"'Hallo {name} Welt\",\n  \"fooSpace\": {\n    \"Hello world {name}\": \"'Hallo {name} Welt in Space\"\n  }\n}\n```\n\nYou can see that with those sub-objects, we can manage translation modules,\nand with `{variableName}` syntax, we manage variable interpolation.\n\nThis is how it is used:\n\n```ts\nimport { t, changeLanguage, TFunction } from 'kiss-arch'\n\n// language defaults to: en\n// changing to german here\nchangeLanguage('de')\n\n// leads to: \"Hallo Welt, Mellon\"\nt('Hello world {name}', { name: 'Mellon' })\n\n// and back to english\nchangeLanguage('en')\n\n// leads to: \"Hello world, Mellon\"\nt('Hello world {name}', { name: 'Mellon' })\n\n// translating from a module\nconst tFoo = t('fooSpace') as TFunction\n\n// leads to: \"Hello world, Mellon in Space\"\ntFoo('Hello world {name}', { name: 'Mellon' })\n```\n\nIf there is no translation, a warning message will be printed to `console`\nin case `getMode()` returns `development`, and the key will be rendered.\n\n## Test\n\n    yarn test\n\nThis library comes with substantial test coverage \u003e 90%.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkyr0%2Fkiss-arch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkyr0%2Fkiss-arch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkyr0%2Fkiss-arch/lists"}