{"id":20109776,"url":"https://github.com/hyper63/next-clean-architecture","last_synced_at":"2026-03-03T14:31:31.146Z","repository":{"id":52566656,"uuid":"516512979","full_name":"hyper63/next-clean-architecture","owner":"hyper63","description":"Clean Architecture with NextJS, using Hyper as a services tier","archived":false,"fork":false,"pushed_at":"2024-09-02T20:12:51.000Z","size":806,"stargazers_count":2,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-11-09T02:02:01.726Z","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":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyper63.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,"zenodo":null}},"created_at":"2022-07-21T20:29:10.000Z","updated_at":"2024-09-02T20:12:56.000Z","dependencies_parsed_at":"2024-08-01T23:41:26.454Z","dependency_job_id":"ceaec974-0afd-40ff-82d4-4ef3ffe1ef7d","html_url":"https://github.com/hyper63/next-clean-architecture","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hyper63/next-clean-architecture","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyper63%2Fnext-clean-architecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyper63%2Fnext-clean-architecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyper63%2Fnext-clean-architecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyper63%2Fnext-clean-architecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyper63","download_url":"https://codeload.github.com/hyper63/next-clean-architecture/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyper63%2Fnext-clean-architecture/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283447137,"owners_count":26837401,"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","status":"online","status_checked_at":"2025-11-09T02:00:05.828Z","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":[],"created_at":"2024-11-13T18:09:24.335Z","updated_at":"2025-11-09T02:02:16.282Z","avatar_url":"https://github.com/hyper63.png","language":"TypeScript","readme":"# NextJS Clean Architecture\n\nThis is a [Next.js](https://nextjs.org/) project bootstrapped with\n[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).\n\nThis stack employs the tenants of Clean Architecture, using\n[`hyper`](https://hyper.io) as general purpose services tier\nand [`graphql`](https://graphql.org/) to expose a presentation model.\n\n\u003c!-- toc --\u003e\n\n- [Getting Started](#getting-started)\n- [Architecture](#architecture)\n  - [Stack](#stack)\n  - [Data Flow](#data-flow)\n    - [A Note on Context](#a-note-on-context)\n    - [GraphQL](#graphql)\n- [Relevant Code](#relevant-code)\n  - [Business Logic and Entities](#business-logic-and-entities)\n- [Testing](#testing)\n  - [Unit Tests](#unit-tests)\n  - [Contract Tests](#contract-tests)\n- [Domains](#domains)\n\n\u003c!-- tocstop --\u003e\n\n## Getting Started\n\n\u003e If you're using Gitpod, all of this is done for you\n\nYou will need to enable [corepack](https://nodejs.org/api/corepack.html) by running `corepack enable`,\nso that `yarn berry` can be used. Then run `yarn set version berry`.\n\n\u003e `yarn berry` supports using `node_modules`, and is completely backwards compatible with `yarn 1.x`\n\nRun `yarn` to install dependencies\n\nYou'll need to create a `.env` file. You can generate one with default values by\nrunning `node env.cjs`.\n\n```text\nHYPER=...\n```\n\nThen:\n\n```\nyarn dev\n```\n\nThis will start:\n\n- [NextJS](https://nextjs.org/) on\n  [http://localhost:3000](http://localhost:3000)\n- [`hyper-nano`](https://blog.hyper.io/introducing-hyper-nano-hyper-cloud-in-a-bottle/)\n  will be downloaded, started on port `6363`, and a `data` and `cache` service bootstrapped\n\nThere is a `graphql` at `/api/graphql` equipped with the [`GraphiQL Playground`](https://github.com/graphql/graphiql)\n\nThere is also a serveless function endpoint at `/api/hello`\n\nThese both demonstrate how dependencies may be injected, so that even endpoints and graphql resolvers can be easily unit tested.\n\n## Architecture\n\n### Stack\n\nFrontend:\n\n- React/[NextJS](https://nextjs.org/)\n- [TailwindCSS](https://tailwindcss.com/)\n\nBackend:\n\n- [NextJS](https://nextjs.org/)\n- [GraphQL](https://graphql.org) ([`graphql-helix`](https://github.com/contra/graphql-helix))\n\nTooling/Utility:\n\n- Zod (for schema parsing and validation)\n- hyper-connect (hyper SDK)\n- ramda\n- prettier, typescript, eslint, husky, lint-staged (code style,linting)\n\nThis application uses [hyper](https://hyper.io) as a services tier, using the [data](https://hyper.io/product#data) and [cache](https://hyper.io/product#cache) services.\n\nBoth the business (or entity) models and document models are simple [`zod`](https://github.com/colinhacks/zod) schemas. It's important to keep the persistence model and your business model decoupled, so that each is able to diverge from each other. This helps ensure the persistence layer remains an implementation detail.\n\nThe presentation tier uses [GraphQL](https://graphql.org) to power the presentation model. Again, it's important to keep the presentation model and the business model decoupled, so that each is able to diverge from each other.\n\n### Data Flow\n\n![](./docs/dataflow.png)\n\nThe structure of this project follows the tenants of Clean Architecture.\n\nThe way data flows through this application is very purposefully consistent, as it encourages business logic encapsulation, efficient data fetching, and clear separation of concerns.\n\nData in the backend typically flows through these layers:\n\n```text\nGraphQL -\u003e Business API -\u003e dataloader|client (effects)\n# OR\nREST -\u003e Business API -\u003e dataloader|client (effects)\n```\n\nData flows into the backend either via REST endpoints, or GraphQL. Almost all communication from the frontend ought to be done via GraphQL, but REST can also be used. For example, file upload is notoriously \"weird\" using GraphQL, so using a regular HTTP endpoint, that still calls into business services, may be preferred.\n\n\u003e **The frontend should almost always use GraphQL to communicated with the backend**\n\n#### A Note on Context\n\nEvery request, GraphQL or REST or `getServerSideProps`, flows through the `middleware` composed onto `getServerSideProps` and endpoints in `/pages/api`. The `middlewares` are located in `/lib/middleware`.\n\nThe `middleware` is where an application `context` is built out, and attached to the Request. This `context`, which is unique per request, is where all downstream execution will derive their application dependencies. You can think of Context as an IoC container of the instantiated dependencies. Each layer in the backend will use `context` to access subsequent layers.\n\n\u003e As a general rule of thumb, `context` is where each layer finds its dependencies. It is strongly typed, so you shouldn't have issues finding what you need. If you're trying to import a dependency, it better be a `model` from `/lib/domain/models` or a utility installed via `npm` ie. `ramda`. Application deps ie. `apis`, `dataloaders`, `clients`, `config`, should all be accessed via `context`, in any layer.\n\n#### GraphQL\n\nThe `/lib/graphql` folder has the following structure:\n\n```text\n├── mutation.schema.ts\n├── query.schema.ts\n├── [type].schema.ts\n├── resolvers.ts \u003c-- resolver utilities ie. reusuable pieces\n├── schema.ts \u003c-- all \"mini-schemas\" are combined\n```\n\nEach GraphQL type \"mini-schema\" exports `typeDefs`, `resolvers` and `transformers` at the top level. `transformers` are primarily for `directives` to weave additional functionality into the Graph.\n\nEach GraphQL resolver receives any application level dependencies via the `context` argument (the third argument passed to every resolver). To obtain data, **GraphQL resolvers should almost always call into `apis`**. A resolver should **never** call into `dataloaders` or `clients`.\n\n## Relevant Code\n\n### Business Logic and Entities\n\nAll business logic and entities can be found in [./lib/domain](./lib/domain/)\n\nIn particular [./lib/domain/models](./lib/domain/models) contains all our entities and business rules. All of this logic has no dependencies other than utilities ie. `ramda`, `zod`, etc. and each other, and therefore is very easy to unit test!\n\n[./lib/domain/apis](./lib/domain/apis) weaves side effects, received via simple dependency injection, with our business rules.\n\n\u003e Read more on business logic encapsulation and Clean Architecture\n\u003e [here](https://blog.hyper.io/clean-architecture-at-hyper/)\n\nYou can access business services in an\n[API Route Handler](https://nextjs.org/docs/api-routes/introduction) by wrapping\nthe handler with the [`withMiddleware`](./lib/middleware/index.ts)\nmiddleware. This will add the `DomainContexrt` on the `NextApiRequest` at\n`req.domain`.\n\nYou can access business services in\n[`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props)\nby wrapping the function with the\n[`withMiddlewareSsr`](./lib/middleware/index.ts) middleware. This will add\nthe `DomainContexrt` on the `GetServerSidePropsContext` at\n`context.req.domain`.\n\n## Testing\n\n**This application only tests business logic. It does not test anything on the frontend.** Frontend code should be very dumb and it typically is updated much more frequently than business logic rules do. Business logic is the most important part of the application. It is much worse to have data integrity issues, or bugs in the business logic, than it is to have a visual bug in the frontend.\n\nThat being said, there is nothing stopping you from testing frontend application code, with something like Cypress or Playwright. Just keep in mind that if you're _having_ to test the presentation layer to assert whether or not _business logic_ is correct, that is a smell that you have a leak in your domain and are coupling layers that ought to stay decoupled.\n\n### Unit Tests\n\nCurrently all unit tests live in the `lib` folder. All test files are named with the format `[ORIGINAL_FILE].test.ts`. The application uses [vitest](https://vitest.dev/api/) for unit testing. You can run the tests with `yarn test` or `yarn test:ci` for code coverage reports.\n\nAll Business Logic should be tested. This mainly includes `apis`, `models`, `utils`, and some `graphql` related code. `Dataloaders` and `clients` are not tested, as they are thin wrappers around external services. Most `graphql` resolvers are not tested, as they are thin wrappers around `apis` and `models`.\n\nWhen writing tests, you should mock out any _dependencies_ that are not being tested. If it's injected, it's a dependency.\n\n\u003e **Never mock business logic under test, because then you're testing a mock, not your business rules**.\n\n### Contract Tests\n\nYou may want to ensure the correct shape of data is being passed between dependencies. TypeScript will get you most of what you want here, but recognize this is only a **build time** check **not** a runtime check. In other words, **types could be wrong, and a false sense of security**. If it's crucial for data to be passed in a particular shape ie. to a 3rd party, you can use an inline \"Contract\" test. A `zod` schema that parses input into a dependency works perfectly for this:\n\n```ts\nimport z from 'zod'\n\nconst input = { no: 'bar' }\nconst veryImportantContact = z.object({ foo: z.string().min(1) })\n\ndep.fooApi(veryImportantContract.parse(input)) // contract will throw and the dep is never invoked.\n```\n\nThis contract is excercised every time your dependency is consumed, at _runtime_. This also ensures your mock that you use in your [Unit Tests](#unit-tests) stay up to date, as it is also exercised by the Contract.\n\n## Domains\n\nThis project is structured as a\n[\"Modular Monolith\"](https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity).\nThis means that despite only having a single deployable unit, disaperate business logic is encapsulated within separate \"bounded-contexts\" or \"domains\". When one bounded context wishes to communicate to another bounded context, it is done so via an API call.\n\n\u003e In a microservice architecture, each domain may expose an api as a service to be consumed via a network\n\u003e call. In a modular monolith, that API is a function, running on the same process. One deployable\n\u003e -- many domains.\n\nUltimately, the goal of Modular Monolith is to provide a \"sandbox\" for engineering to hone in on\nwhat the domains in the system actually _are_ and to refine the contextual boundaries between them,\nover time, **without** having to pay the heavy costs of getting it wrong, like you would have to in a\nmicroservice architecture ie. lock-step release cycles, api versioning, etc. With a Modular\nMonolith, if a context boundary needs to be changed, then its just a matter of rearranging code\nbehind the logical boundaries, which is then all deployed as a single unit.\n\n\u003e The ultimate goal is establish domain boundaries that enable loose coupling between each of the domains, such that one domain can change and deploy independent of the others, as long as its api has not changed.\n\nAt a certain point, when it becomes necessary to break out separate deployable units ie.\nmicroservices, for the purposes of scalability, team independence, etc. The context boundaries are\nmore well defined, and the whole vertical slice can be moved into a separate deployable, including domain\nmodels, shared boilerplate, and persisted data moved into it's own database.\n\n\u003e This is akin to a strangler pattern where functionality can be slowly \"strangled\" out of the\n\u003e monolith, until it becomes non-existent. What's left are the domain-driven microservices.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyper63%2Fnext-clean-architecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyper63%2Fnext-clean-architecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyper63%2Fnext-clean-architecture/lists"}