{"id":18466776,"url":"https://github.com/kwasniew/hyperapp-realworld-example-app","last_synced_at":"2025-06-30T19:32:54.246Z","repository":{"id":41811933,"uuid":"142662527","full_name":"kwasniew/hyperapp-realworld-example-app","owner":"kwasniew","description":"A Single Page Application written in Hyperapp 1 https://hyperapp.netlify.com/","archived":false,"fork":false,"pushed_at":"2022-12-07T21:53:00.000Z","size":1829,"stargazers_count":47,"open_issues_count":15,"forks_count":10,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-08T08:46:36.653Z","etag":null,"topics":["elm-architecture","hyperapp","javascript","realworld","realworld-frontend","single-page-app"],"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/kwasniew.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}},"created_at":"2018-07-28T08:57:50.000Z","updated_at":"2024-11-23T20:36:18.000Z","dependencies_parsed_at":"2023-01-24T23:01:43.700Z","dependency_job_id":null,"html_url":"https://github.com/kwasniew/hyperapp-realworld-example-app","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kwasniew/hyperapp-realworld-example-app","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwasniew%2Fhyperapp-realworld-example-app","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwasniew%2Fhyperapp-realworld-example-app/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwasniew%2Fhyperapp-realworld-example-app/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwasniew%2Fhyperapp-realworld-example-app/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kwasniew","download_url":"https://codeload.github.com/kwasniew/hyperapp-realworld-example-app/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwasniew%2Fhyperapp-realworld-example-app/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262838417,"owners_count":23372529,"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":["elm-architecture","hyperapp","javascript","realworld","realworld-frontend","single-page-app"],"created_at":"2024-11-06T09:17:30.157Z","updated_at":"2025-06-30T19:32:54.218Z","avatar_url":"https://github.com/kwasniew.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/kwasniew/hyperapp-realworld-example-app.svg?branch=master)](https://travis-ci.org/kwasniew/hyperapp-realworld-example-app)\n\n# ![RealWorld Example App](./logo.png)\n\n\u003e ### Hyperapp V1 codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.\n\n\n### [Demo](https://hyperapp.netlify.com/)\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;[RealWorld](https://github.com/gothinkster/realworld)\n\n\nThis codebase was created to demonstrate a fully fledged fullstack application built with [Hyperapp V1](https://github.com/hyperapp/hyperapp) including CRUD operations, authentication, routing, pagination, and more.\n\nFor more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.\n\n\n# How it works\n\nThe section below is not intended as a beginner's intro to hyperapp, but rather some notes with tradeoffs to consider \nwhen building a bigger application with Hyperapp.\n\n## Build\n\nI use [parcel](https://parceljs.org/) to automate application build. Ideally I'd like to avoid bundling and transpilation\nduring development as most modern browsers support the latest ES features. \nUnfortunately browsers can't resolve npm based dependencies with imports. \nAlso, browsers don't understand JSX. My main motivation for using JSX over ES6 tagged template literals is better \ncode formatting support in most tools.\n\nindex.html is our main entry point and parcel resolves graph of dependencies from there. First it goes to src/main.js and then\nresolves all the other JS dependencies.\n\n## App structure\n\nmain.js - injects browser specific dependencies to our app. It's only used for production code, in tests we can inject test doubles\ninstead of real fetch or localStorage. In the remaining parts of my codebase I try to avoid using window or any other global object\nin favor of argument passing (dependency injection).\n\napp.js - this is where we build main **dependency graph** for our application. Here we initialize localStorage based **session repository** and fetch\nbased **API gateway** and inject them into corresponding action factories. \n\n**Actions** (article, articles, auth, editor, profile, settings) are split along **business capabilities**.\nActions manage their parts of the **state**.\nTo keep actions testable we always inject all heavy dependencies. \nScaling application logic and state follows a simple strategy: if a given object gets too big split it into smaller objects and delegate.\n\nAll view code resides in one view directory and consists only of **view functions/page fragments**. \nScaling view code follows a simple strategy: if a given view fragment gets too big split it into smaller fragments and delegate.\n\nHyperapp itself expects state, actions, view and mount point. app function is our main contact point with the framework.\n```\napp(state, actions, view, document.body)\n```\nIn app.js we provide initial value of the application state, then we aggregate actions from all the modules that make up the app\nand finally we provide aggregate view of the entire app. \n\n## Data model\n\nState/model is inspired by [Elm RealWorld](https://github.com/rtfeldman/elm-spa-example/).\nTo keep it simple I don't use things like Maybe types as they don't seem to be idiomatic in the JS world.\nData modelling is where I missed Elm like static type system the most.\n\nHyperapp enforces one big state object and doesn't allow you to mutate it. It raises 2 questions:\n* how to get deeply nested fields w/o duplicating code in many places\n* how to update deeply nested field in the immutable world\n\nUsually people write selectors to solve the first problem.  \nTo overcome the second issue we either use Object.assign() or spread objects extensively.\n\n\nLenses (e.g. provided by ramda) solve this problem is a more composable, realiable and elegant way.\n\n\nInstead of writing\n```\nconst authorSelector = state =\u003e state.page.article.author // read\n{...state, page: {...state.page, article: {...state.page.article, author: 'new author'}}} //write\n```\nwe can do the following\n```\nR.view(R.lensPath(['page', 'article', 'author']), state) //read\nR.set(R.lensPath(['page', 'article', 'author']), 'new author', state) // simple write\nR.over(R.lensPath(['page', 'article', 'author']), R.toUpper, state) // more complex write \n```\n\nPlease note that there's a symmetry between getting and setting the data. \n\nFor state derived values/selectors I decided to write some JS code on the top of lenses view without any extra library like [reselect](https://github.com/reduxjs/reselect).\nI can add memoization on top when needed. However my runtime performance analysis didn't show bottlenecks in this area so far. \n\n## Views\n\nAll view related code is pure functions/view fragments/stateless components mapping state to Virtual DOM in JSX.\n\nI reused most of the code from [React-Redux RealWorld](https://github.com/gothinkster/react-redux-realworld-example-app).\nThe main difference is that there's only functions and no components with local state. I find it easier to reason about\nand manage one state object rather than state split between different components. \n\nI decided to pass all function arguments explicitly at the expense of more boilerplate. Another option would be to use\n[lazy components](https://github.com/hyperapp/hyperapp#lazy-components) which is basically Hyperapp equivalent\nof React's mapStateToProps/mapDispatchToProps where we allow each view fragment to peek inside state and actions.\n\nCompared to React conventions, hyperapp favors native DOM events and attributes (e.g onclick over onClick and class over className).\nThis is nice since I can transfer my DOM knowledge.\n\n## Routing\n\nRouting state is part of the main state object. Router also enhances actions with a history.pushState() wrapper (actions.location.go).\nI copy pasted parts of the original [@hyperapp/router](https://github.com/hyperapp/router) that I found useful and they\nlive in the router directory (locations and parseRoute). \nI don't use Route components modelled after React Router components as they encourage data fetching from lifecycle hooks\nwhich I don't like.\nI also prefer regular anchor tags over Link components which don't allow to inject API calls before we change URL. \nI use [internal-nav-helper](https://github.com/HenrikJoreteg/internal-nav-helper)\nto have a single click handler in the root element of the application. By default all links end up in this common handler unless you\nopt out by stopping event propagation.\n\nClient side routing cycle looks like this: \n* click a link with href \n* load new page (set new page name based on href and preload its data from API)\n* dynamically load page by name (API data arrives in the background)\nThis way our view functions don't need any oncreate/onupdate hooks (think componentDidMount and co. in the React world).\nWe select views dynamically based on the current page name, not on the current state of the URL.\n\nTo make back button work correctly we also track popstate events.\n\n## API and async patterns\n\nMost async code in this app revolves around API interactions that are simple enough to be handled with promises (no sagas/observables etc.).\nI factored out repeatable async flows into helper functions. E.g. actions/forms.js has withErrorHandling function\nthat keeps track of starting/stoping async requests and handling potential errors.\nFuture version of Hyperapp should have the concept of \"effects as data\" similar to Elm and I'm looking forward to it as \nit should make testing easier. \n\n## Error handling\n\nI only did the minimum to replicate basic form error handling (client errors) as shown in the reference React version.\nWe could go one step further and add better server error handling for API errors/network failures.\n\n## JS - the best parts\n\nIn this codebase I try to use simple and predictable language constructs so I avoid this (unknown behavior without looking\nat call site) and classes (way too complex [mental model](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch6.md#mental-models-compared)). \nThe code is mostly based on functions, closures and object literals. \n\n## Code size\n\n[JS startup performance](https://medium.com/reloading/javascript-start-up-performance-69200f43b201) is a problem for many SPAs.\nTo stay within a [reasonable performance budget](https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/)\nyour framework should be as tiny as possible. One could even argue that this kind of app shouldn't be a SPA in the first place.\nAnyway, I digress. \nHyperapp itself is only around 1kb gzipped and minified which is 400 lines of code. It's not only good for performance reasons\nbut also allows me to understand the framework code.\n\nThe whole app weights around 25kB gzipped/minified split equally between my app code and libraries (mostly functional\n utilities and markdown parser).\n \n ![App](https://i.imgur.com/TZ1GD5Q.png) \n \n Application code itself is about 2200 LoC split equally between views and application logic. \n It makes it comparable to [React-Redux RealWorld](https://github.com/gothinkster/react-redux-realworld-example-app) version in terms of the \n userland codebase size.\n \n ## Startup time\n \nClean start is about 5 seconds on my Macbook Air and with parcel caching enabled it's about 1 second.\n\n## Tests\n\nBecause all moving parts as exposed as arguments we can explicitly pass test doubles instead hacking import mechanism with jest.mock.\nI find the jest clean startup times a little bit too slow but decided to use it anyway because of the convenience of snapshots and jsdom.\n\n### Slow: Approval text based/snapshot tests with jsdom\n\n```npm run test:slow```\n\nI use approval snapshot tests for the entire app. We simulate user interacting with our app and take snapshots after\neach significant action. The tests rely on built-in jsdom support in jest so there's no need to spin up a browser.\nYet the tests still take about [3 seconds](https://github.com/jsdom/jsdom/issues/1454). \nTo automate interactions and waiting for elements to appear in async fashion I use [dom-testing-library](https://github.com/kentcdodds/dom-testing-library).\n\nTo stub out localStorage and fetch calls I created manual test doubles. It makes our tests predictable.\nSnapshots don't capture user intent very well but on the other hand are really cheap to create. It's a tradeoff I'm willing to make here\nas the role of this tests will be to provide coarse grained verification.\nAs the code grows we can add page objects/screenplays/another technique du jour in the testing world.\n\n### Fast: Unit tests\n\n```npm run test:fast```\n\nI want to have [subsecond feedback](https://skillsmatter.com/skillscasts/9971-testable-software-architecture-with-aslak-hellesoy) from those tests even with clean start. \nThose tests live next to code they test.\nSince we clearly separated state and actions from the view, we can test state transitions very easily.\nThe test format follows this template: \n* given some initial state\n* trigger some action/behavior\n* verify state afterwards.\n\nSome state transitions are two step. First we get immediate and synchronous state transition. Then it's followed by the async state transition.\nEach async action returns a promise allowing the test code to wait for it.  \n\nFor IO and modules I'm interacting with I use manually written test doubles (I personally find no need for mocking library in JS). \n\n# Getting started\n\n```\nnpm i\nnpm start\nnpm test\n```\n\nGo to: [http://localhost:1234]()\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkwasniew%2Fhyperapp-realworld-example-app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkwasniew%2Fhyperapp-realworld-example-app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkwasniew%2Fhyperapp-realworld-example-app/lists"}