{"id":29694088,"url":"https://github.com/allmonday/resolver-vs-graphql","last_synced_at":"2025-07-23T08:38:08.666Z","repository":{"id":298666152,"uuid":"1000691482","full_name":"allmonday/resolver-vs-graphql","owner":"allmonday","description":"an interesting data orchestration solution","archived":false,"fork":false,"pushed_at":"2025-06-29T13:09:41.000Z","size":297,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-06-29T14:24:05.667Z","etag":null,"topics":["bff-api","fastapi","graphql","pydantic"],"latest_commit_sha":null,"homepage":"","language":"Python","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/allmonday.png","metadata":{"files":{"readme":"README-en.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":"2025-06-12T07:15:23.000Z","updated_at":"2025-06-29T13:09:43.000Z","dependencies_parsed_at":"2025-06-16T06:40:42.239Z","dependency_job_id":null,"html_url":"https://github.com/allmonday/resolver-vs-graphql","commit_stats":null,"previous_names":["allmonday/compare-graphql-vs-rest-resolver","allmonday/compare-graphql-vs-resolver","allmonday/resolver-vs-graphql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/allmonday/resolver-vs-graphql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fresolver-vs-graphql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fresolver-vs-graphql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fresolver-vs-graphql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fresolver-vs-graphql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/allmonday","download_url":"https://codeload.github.com/allmonday/resolver-vs-graphql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/allmonday%2Fresolver-vs-graphql/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266646486,"owners_count":23961954,"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-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"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":["bff-api","fastapi","graphql","pydantic"],"created_at":"2025-07-23T08:38:03.695Z","updated_at":"2025-07-23T08:38:08.646Z","avatar_url":"https://github.com/allmonday.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Resolver Pattern: A Better Choice than GraphQL in BFF Scenarios\n\n[中文](./README.md)\n\nThis is a comparison project between Resolver pattern and GraphQL (strawberry) pattern based on FastAPI.\n\nFocuses on the best development pattern for **internal frontend-backend API communication** scenarios\n\n\u003e Also applicable to BFF (Backend for Frontend) scenarios\n\n\u003e Assumes readers are familiar with GraphQL and RESTful, basic concepts will not be elaborated.\n\nComparison scenarios include:\n\n- [x] Associated data fetching and construction\n- [x] Query parameter passing\n- [x] Frontend query method comparison\n- [x] Post-processing data at each node, minimal cost view data construction (key focus)\n- [x] Architecture and refactoring differences\n\n## Introduction\n\nGraphQL is an excellent API query tool, widely used in various scenarios. However, it's not a universal solution and encounters various problems in different scenarios.\n\nThis article specifically targets the common scenario of \"internal frontend-backend API integration\", analyzes the problems with GraphQL, and attempts to solve them one by one using the Resolver pattern based on `pydantic-resolve`.\n\nLet me briefly introduce what the Resolver pattern is: It's a pattern that extends existing RESTful interfaces by introducing resolver and post-processing concepts, transforming originally \"generic\" RESTful interfaces into RPC-like interfaces that are customized for frontend pages.\n\nIn the Resolver pattern, we extend and combine data based on Pydantic classes (dataclass can also be used).\n\nHere's a code example that demonstrates the ability to fetch associated data and generate view data after post-processing. The article will gradually explain all features and design intentions later.\n\n```python\nclass Story(BaseStory):\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):\n    id: int\n    point: int\n\n    name: str\n    def resolve_name(self, ancestor_context):\n        return f'{ancestor_context[\"sprint_name\"]} - {self.name}'\n\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\n    done_perc: float = 0.0\n    def post_done_perc(self):\n        if self.tasks:\n            done_count = sum(1 for task in self.tasks if task.done)\n            return done_count / len(self.tasks) * 100\n        else:\n            return 0\n\nclass Sprint(BaseSprint):\n    __pydantic_resolve_expose__ = {'name': 'sprint_name'}\n\n    simple_stories: list[SimpleStory] = []\n    def resolve_simple_stories(self, loader=LoaderDepend(StoryLoader)):\n        return loader.load(self.id)\n\n\n@router.get('/sprints', response_model=list[Sprint])\nasync def get_sprints():\n    sprint1 = Sprint(\n        id=1,\n        name=\"Sprint 1\",\n        start=datetime.datetime(2025, 6, 12)\n    )\n    sprint2 = Sprint(\n        id=2,\n        name=\"Sprint 2\",\n        start=datetime.datetime(2025, 7, 1)\n    )\n    return await Resolver().resolve([sprint1, sprint2] * 10)\n```\n\nIt can act as a BFF layer. Compared to traditional BFF tools, each layer introduces \"post-processing\" methods, making many aggregation calculations that originally required traversal expansion as easy as pie.\n\nFor more features of pydantic-resolve, please see [https://github.com/allmonday/pydantic-resolve](https://github.com/allmonday/pydantic-resolve)\n\n## Getting Started\n\n1. Install dependencies:\n   ```sh\n   python -m venv venv\n   source venv/bin/activate  # Windows users please replace accordingly\n   pip install -r requirements.txt\n   ```\n2. Start the service:\n   ```sh\n   uvicorn app.main:app --reload\n   ```\n3. Open [http://localhost:8000/graphql](http://localhost:8000/graphql) to access GraphQL playground.\n4. Open [http://localhost:8000/docs](http://localhost:8000/docs) to view Resolver pattern\n\n## 1. Data Fetching and Composition\n\n```sh\nuvicorn app.main:app --reload\n```\n\nResolver itself is one of the two core features of GraphQL (the other being Query functionality). Through Resolver and dataloader, GraphQL can freely compose data.\n\nIn GraphQL, data definition can be graph-based, but in practice, for each specific query, the Query structure is tree-like. This is why queries cannot just write object names without providing specific fields.\n\nThis is a valid query:\n\n```graphql\nquery MyQuery {\n  sprints {\n    id\n    name\n    start\n    stories {\n      id\n      name\n      owner\n    }\n  }\n}\n```\n\nThis is an invalid query:\n\n```graphql\nquery MyQuery {\n  sprints {\n    id\n    name\n    start\n    stories # playground will show red error\n  }\n}\n```\n\nBecause if the stories field has object types, GraphQL doesn't know whether to continue expanding. Therefore, essentially, Query serves as the driving basis (configuration) for resolvers.\n\nIn the Resolver pattern, **Query statements are hardcoded into the code**, describing the desired combined data through inheritance and extension of pydantic classes.\n\n\u003e This approach loses query flexibility but becomes more suitable for RPC usage scenarios, namely the internal API integration scenarios mentioned at the beginning of the article, allowing data consumers to not bear the additional burden of query statements.\n\u003e How to determine if you're in this scenario? The simplest example is if your Query uses all fields of objects in specific entry points, then you probably belong to this scenario.\n\nIf you directly inherit BaseStory, all fields of BaseStory will be returned. You can also define a new class and declare the required fields in it. The `@ensure_subset` decorator is provided to additionally ensure that field names actually exist in BaseStory.\n\n```python\nclass Story(BaseStory):\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\n\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):  # how to pick fields..\n    id: int\n    name: str\n    point: int\n\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\nclass Sprint(BaseSprint):\n    stories: list[Story] = []\n    def resolve_stories(self, loader=LoaderDepend(StoryLoader)):\n        return loader.load(self.id)\n```\n\nAnother difference from GraphQL concepts is that GraphQL's input is user query statements, while Resolver's input data is root node data. This might be a bit abstract to say directly, so a code comparison would be more illustrative:\n\nIn GraphQL, when users query sprints, the root data fetching happens within the sprints method.\n\n```python\n@strawberry.type\nclass Query:\n    @strawberry.field\n    def hello(self) -\u003e str:\n        return \"hello world\"\n\n    @strawberry.field\n    async def sprints(self) -\u003e List[Sprint]:\n        sprint1 = Sprint(\n            id=1,\n            name=\"Sprint 1\",\n            start=datetime.datetime(2025, 6, 12)\n        )\n        sprint2 = Sprint(\n            id=2,\n            name=\"Sprint 2\",\n            start=datetime.datetime(2025, 7, 1)\n        )\n        return [sprint1, sprint2] * 10\n```\n\nIn Resolver, you need to provide root data and pass it to the `Resolver().resolve` method for parsing.\n\n```python\n@router.get('/sprints', response_model=list[Sprint])\nasync def get_sprints():\n    sprint1 = Sprint(\n        id=1,\n        name=\"Sprint 1\",\n        start=datetime.datetime(2025, 6, 12)\n    )\n    sprint2 = Sprint(\n        id=2,\n        name=\"Sprint 2\",\n        start=datetime.datetime(2025, 7, 1)\n    )\n    return await Resolver().resolve([sprint1, sprint2] * 10)\n```\n\nThis approach is very friendly to traditional RESTful interfaces. For example, an interface that originally returns flat BaseSprint objects can be seamlessly extended by simply modifying the response_model definition type.\n\n```python\n@router.get('/base-sprints', response_model=list[BaseSprint])\nasync def get_base_sprints():\n    sprint1 = BaseSprint(\n        id=1,\n        name=\"Sprint 1\",\n        start=datetime.datetime(2025, 6, 12)\n    )\n    sprint2 = BaseSprint(\n        id=2,\n        name=\"Sprint 2\",\n        start=datetime.datetime(2025, 7, 1)\n    )\n    return [sprint1, sprint2] * 10\n```\n\nOf course, if you want to mimic GraphQL's style, it's also easy:\n\n```python\nclass Query(BaseModel):\n    sprints: list[Sprint] = []\n    async def resolve_sprints(self):\n        sprint1 = Sprint(\n            id=1,\n            name=\"Sprint 1\",\n            start=datetime.datetime(2025, 6, 12)\n        )\n        sprint2 = Sprint(\n            id=2,\n            name=\"Sprint 2\",\n            start=datetime.datetime(2025, 7, 1)\n        )\n        return [sprint1, sprint2]\n\n# resolve\nawait Resolver().resolve(Query())\n```\n\nThat's it.\n\nAnother capability of Resolver is handling self-referential type data. Because it doesn't need to provide Query statements like GraphQL, the construction logic for self-referential types (like Tree) can be completely managed by the backend.\n\nFor comparison, in GraphQL, queriers need to write very deep query statements like this because they don't know the actual depth:\n\n```graphql\nquery MyQuery {\n  tree {\n    id\n    children {\n      id\n      children {\n        id\n        children {\n          id\n          children {\n            id\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nThere's also the possibility of insufficient depth description requiring further query adjustments.\n\nIn contrast, in Resolver or traditional RESTful mode, you just need to define the type and return value:\n\n```python\n@router.get('/tree', response_model=list[Tree])\nasync def get_tree():\n    return [Tree(id=1, children=[\n        Tree(id=2, children=[Tree(id=3)])\n    ])]\n```\n\nThen a simple `curl http://localhost:8000/tree` gets it done. The depth problem is solved by backend-specific logic.\n\n## Query Parameter Passing\n\n```sh\nuvicorn app_filter.main:app --reload\n```\n\nGraphQL can receive parameters at each node, with each resolver able to accept a set of params:\n\n```python\n@strawberry.type\nclass Sprint:\n    id: int\n    name: str\n    start: datetime.datetime\n    task_count: int = 0\n    @strawberry.field\n    async def stories(self, info: strawberry.Info, ids: list[int]) -\u003e List[\"Story\"]:\n        stories = await info.context.story_loader.load(self.id)\n        return [s for s in stories if s.id in ids]\n```\n\nHowever, in practice, for convenience of dynamic setting, they are generally managed centrally at the top:\n\n```graphql\n// query\nquery MyQuery($ids: [Int!]!) {\n  sprints {\n    start\n    name\n    id\n    stories(ids: $ids) {\n      id\n      name\n      owner\n      point\n      tasks {\n        done\n        id\n        name\n        owner\n      }\n    }\n  }\n}\n\n// variables\n{\n  \"ids\": [1]\n}\n```\n\nThis implies a centralized parameter management design philosophy. Although parameter consumers are at various nodes, centralized access can be achieved through agreed variable aliases.\n\nIn Resolver mode, since queries are already \"hardcoded\" through code in advance, all parameters can be provided through global variables like context:\n\n```python\nclass Sprint(BaseSprint):\n    simple_stories: list[SimpleStory] = []\n    async def resolve_simple_stories(self, context, loader=LoaderDepend(StoryLoader)):\n        stories = await loader.load(self.id)\n        stories = [s for s in stories if s.id in context['story_ids']]\n        return stories\n\n@router.get('/sprints', response_model=list[Sprint])\nasync def get_sprints():\n    sprint1 = Sprint(\n        id=1,\n        name=\"Sprint 1\",\n        start=datetime.datetime(2025, 6, 12)\n    )\n    sprint2 = Sprint(\n        id=2,\n        name=\"Sprint 2\",\n        start=datetime.datetime(2025, 7, 1)\n    )\n    return await Resolver(\n        context={'story_ids': [1, 2, 3]},\n    ).resolve([sprint1, sprint2] * 10)\n```\n\nThis is formally equivalent to GraphQL's approach.\n\nYou might notice that filtering stories after fetching them in the provided ids usage is a very inefficient approach. A better way would obviously be to pass it to the Dataloader and let it complete the filtering during data fetching.\n\nIn GraphQL scenarios, Dataloader can only be configured through the params in the `loader.load(params)` method, so achieving this functionality requires some awkward writing, such as passing both id and ids together through params to the dataloader:\n\n```python\n  @strawberry.field\n  async def stories2(self, info: strawberry.Info, ids: list[int]) -\u003e List[\"Story\"]:\n      stories = await info.context.story_loader.load((self.id, ids))\n      return stories\n```\n\nThen extract them inside the Dataloader, where the second parameter in the Tuple is actually quite redundant.\n\n```python\nasync def batch_load_stories_with_filter(input: List[Tuple[int, List[int]]]) -\u003e List[List[\"Story\"]]:\n    await asyncio.sleep(0.01)  # Simulate async DB call\n    sprint_ids = [item[0] for item in input]\n    story_ids = input[0][1] # need extra code to check the length of input\n    sprint_id_to_stories: Dict[int, List[Story]] = {sid: [] for sid in sprint_ids}\n\n    for s in STORIES_DB:\n        if s[\"sprint_id\"] in sprint_id_to_stories:\n            if not story_ids or s[\"id\"] in story_ids:\n              sprint_id_to_stories[s[\"sprint_id\"]].append(Story(id=s[\"id\"], name=s[\"name\"], owner=s[\"owner\"], point=s[\"point\"]))\n    return [sprint_id_to_stories[sid] for sid in sprint_ids]\n```\n\nIn the Resolver's multi-entry mode, this problem is very simple to solve. Just add the story_ids field directly to the Dataloader class:\n\n```python\nclass StoryLoader(DataLoader):\n    story_ids: List[int]\n    async def batch_load_fn(self, sprint_ids: List[int]) -\u003e List[List[BaseStory]]:\n        await asyncio.sleep(0.01)  # Simulate async DB call\n        sprint_id_to_stories = {sid: [] for sid in sprint_ids}\n        for s in STORIES_DB:\n            if s[\"sprint_id\"] in sprint_id_to_stories:\n                if not self.story_ids or s[\"id\"] in self.story_ids:\n                    sprint_id_to_stories[s[\"sprint_id\"]].append(s)\n        return [sprint_id_to_stories[sid] for sid in sprint_ids]\n```\n\nThen pass parameters directly in the Resolver() method:\n\n```python\nreturn await Resolver(\n  loader_params={\n      StoryLoader: {\n          'story_ids': [1, 2, 3]\n      },\n  }\n).resolve([sprint1, sprint2] * 10)\n```\n\nAdditionally, `pydantic-resolve` provides parent to access parent node objects and `ancestor_context` to access specific fields from ancestor nodes. These are features that most current GraphQL frameworks don't support. For specific usage, please refer to [ancestor_context](https://allmonday.github.io/pydantic-resolve/api/#ancestor_context), [parent](https://allmonday.github.io/pydantic-resolve/api/#parent).\n\nSummary:\n\n| Parameter Type | Resolver | GraphQL |\n| -------------- | -------- | ------- |\n| Node           | Support  | Support |\n| Global context | Support  | Support |\n| Parent node    | Support  | Limited |\n| Ancestor node  | Support  | None    |\n| Dataloader     | Support  | None    |\n\n## Frontend Query Method Differences\n\nUsing GraphQL, the frontend needs to maintain query statements. Although some people have hardcoded queries into RPC, these all require additional technical complexity.\n\nGenerally, no one would directly use fetch to build GraphQL queries; tools like Apollo client are typically used for querying.\n\nAlso, in the current TypeScript era, to generate frontend type definitions, tools like GraphQL code generator and GraphQL Typescript Generator are needed.\n\nIn Resolver mode, with FastAPI and pydantic, RESTful APIs can be directly generated into SDKs through OpenAPI 3.x, allowing frontends to directly call RPC methods and type definitions, such as openapi-ts.\n\nOpenAPI 3.x is a very mature standard with high stability of various tools. There's also Swagger for viewing API definitions and return types.\n\nAdditionally, writing in Resolver mode is not complex, and it's even feasible for frontends to assemble data themselves (similar to BFF mode), though full-stack mode would be more convenient.\n\n| API                     | Resolver | GraphQL           |\n| ----------------------- | -------- | ----------------- |\n| Provide Query Statement | No       | Yes               |\n| Provide Types           | Support  | Support (complex) |\n| Generate SDK            | Support  | Support (complex) |\n| Frontend Awareness      | Strong   | Relatively weak   |\n\n## Post-processing Data at Each Node, Easy View Data Construction\n\n```sh\nuvicorn app_post_process.main:app --reload\n```\n\nIf the previous comparisons were minor skirmishes, then post-processing capability is the biggest difference between Resolver and GraphQL patterns.\n\nLet me first demonstrate what post-processing is. The following method does several things:\n\n- Modify story.name, adding sprint.name as prefix\n- Calculate story.done_perc based on story.tasks\n\n```python\ndef post_process(sprints: List[Sprint]) -\u003e List[Sprint]:\n    for sprint in sprints:\n        sprint_name = sprint.name\n\n        for story in sprint.simple_stories:\n            story.name = f\"{sprint_name} - {story.name}\"\n            if story.tasks:\n                done_count = sum(1 for task in story.tasks if task.done)\n                done_perc = done_count / len(story.tasks) * 100\n            else:\n                done_perc = 0\n            story.done_perc = done_perc\n\n            for task in story.tasks:\n                ...\n\n    return sprints\n```\n\nYou can see that if this code had more post-processing requirements or more node layers, readability would decline rapidly.\n\nIn Resolver mode, this can be expressed as:\n\n```python\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):\n    ...\n\n    name: str\n    def resolve_name(self, ancestor_context):\n        # Because name already has data, it can be operated even in resolver.\n        # ancestor_context represents variables defined in direct ancestor nodes. Here it refers to sprint.name\n        return f'{ancestor_context[\"sprint_name\"]} - {self.name}'\n\n    done_perc: float = 0.0\n    def post_done_perc(self):\n        if self.tasks:\n            done_count = sum(1 for task in self.tasks if task.done)\n            return done_count / len(self.tasks) * 100\n        else:\n            return 0\n\nclass Sprint(BaseSprint):\n    __pydantic_resolve_expose__ = {'name': 'sprint_name'}\n\n    simple_stories: list[SimpleStory] = []\n    def resolve_simple_stories(self, loader=LoaderDepend(StoryLoader)):\n        return loader.load(self.id)\n```\n\nAncestor node fields are passed through specific ancestor_context without polluting locals.\n\nAnd done_perc relies on local calculation at the node level.\n\nMaintainability improves significantly.\n\n---\n\nIn GraphQL, limited by its Query functionality, post-processing capability can be said to be basically impossible to implement.\n\nMany GraphQL frameworks at most support a post-processing middleware at the root node, where developers can do some processing after all data is fetched.\n\nIn Resolver mode, each node can provide post hooks for additional processing after descendant data processing is completed.\n\nThis is the resolve process, expanding data layer by layer from the ROOT node:\n\n![](./images/resolve.png)\n\nThis is the post-processing process. When all data obtained through resolvers is complete, there's a layer-by-layer return triggering process:\n\n![](./images/post-process.png)\n\nHere's the significance of post-processing methods:\n\n- Can modify fields at each layer node after its descendant fields are all processed, or read descendant node data to implement various statistics or aggregation operations\n  - For example, calculate Story completion rate based on Task.done status\n- Can move node data across layers, such as moving Task nodes under Sprint nodes\n- Can perform cross-layer statistical aggregation, such as counting how many Tasks there are in Sprint by skipping the Story layer transfer\n\nUnfortunately, in GraphQL's design, the concept of post-processing doesn't exist.\n\nUsing GraphQL can only experience a top-down data fetching process. There's no way to implement post-processing at each layer. For example, I cannot know the content of tasks in advance at the story node.\n\nAnd the Query-driven resolver approach constrains the possibility of adding new fields in post-processing methods.\n\nFor example, in Resolver mode, you can use the post_done_perc method to get `self.tasks` information and then calculate the done ratio:\n\n```python\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):  # how to pick fields..\n    id: int\n    name: str\n    point: int\n\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\n    done_perc: float = 0.0\n    def post_done_perc(self):\n        if self.tasks:\n            done_count = sum(1 for task in self.tasks if task.done)\n            return done_count / len(self.tasks) * 100\n        else:\n            return 0.0\n```\n\nIn GraphQL, even if node-level post-processing were somehow supported, for data like done_perc that depends on tasks, if the Query only declares `done_perc` but not `tasks`, then done_perc would error due to lack of tasks data when Query drives the query. If forced to support this, some static analysis process would be needed to analyze the dependency of done_perc on tasks in advance.\n\nIt's precisely the post-processing capability that gives Resolver the ability to easily build view data, making secondary construction and modification based on data possible.\n\nHere are some post-processing features supported by Resolver mode:\n\n| Post-processing Capability                                                                             | Resolver           | GraphQL |\n| ------------------------------------------------------------------------------------------------------ | ------------------ | ------- |\n| Modify current field data [post](https://allmonday.github.io/pydantic-resolve/api/#post)               | Support            | None    |\n| Read current node's descendant data                                                                    | Support            | None    |\n| Send data to descendant nodes [collector](https://allmonday.github.io/pydantic-resolve/api/#collector) | Support            | None    |\n| Hide fields in serialization                                                                           | Support (pydantic) | None    |\n\nYou can also check the code in `app_post_process/rest.py` for more examples.\n\n## Architecture Design Differences\n\nThis section discusses the experience of using GraphQL in project iterations.\n\nThe biggest obstacle when refactoring GraphQL is not daring to modify existing schemas because you don't know which fields in the schema have been queried and which haven't.\n\nThis means that as long as fields have been provided, the basic structure is constrained and can't be easily adjusted, otherwise you'd have to audit all queries to confirm the situation.\n\nDue to GraphQL's flexibility, different teams use it in different ways. Some people build backend-friendly schemas based on ER models, like in our demo, while others build frontend-friendly schemas based on frontend view models, incorporating many post-processing processes. But these two approaches can't be combined because GraphQL lacks powerful post-processing capabilities.\n\n**Summary**: Because GraphQL lacks good post-processing methods, it leads to schema design falling into the dilemma of ER model priority vs view model priority.\n\nGenerally, GraphQL schemas provided by platforms follow the former, designed close to ER models, delegating the process of converting to frontend view data to the queriers.\n\nIn Resolver mode, because the view model consumed by the frontend is actually maintained on the backend, developers have a clear understanding of field usage.\n\nThanks to RESTful's multi-entry points and good inheritance and extension mechanisms, adjustments to each interface won't affect other interfaces.\n\nArchitecturally, Resolver mode matches the objective situation where structural stability gradually decreases in the ER model -\u003e view model process.\n\nBase types in ER models are very stable. Business objects are assembled through inheritance and associated data as needed, then adjusted into view objects through post-processing.\n\nThus, Resolver mode can smoothly build various specific view data required by business while conforming to ER models.\n\n**Summary**: Resolver mode assembles data through specific business based on ER models, then uses post-processing to fine-tune data into expected view data, providing good readability and maintainability.\n\n## Bonus\n\nHow to add post-processing methods to GraphQL?\n\nHere's an interesting approach: remove all resolve methods, remove all Dataloaders, and directly use GraphQL query results as input data.\n\nThen keep all post methods to convert data into expected view objects.\n\n\u003e Because pydantic itself has the ability to load nested data\n\n```python\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):  # how to pick fields..\n    __pydantic_resolve_collect__ = {'tasks': ('task_count', 'task_count2')}  # send tasks to collectors\n\n    id: int\n    name: str\n    point: int\n    tasks: list[BaseTask]\n\n    done_perc: float = 0.0\n    def post_done_perc(self):\n        if self.tasks:\n            done_count = sum(1 for task in self.tasks if task.done)\n            return done_count / len(self.tasks) * 100\n        else:\n            return 0\n\nclass Sprint(BaseSprint):\n    simple_stories: list[SimpleStory]\n    task_count: int = 0\n    def post_task_count(self, collector=Collector(alias='task_count', flat=True)):\n        return len(collector.values())  # this can be optimized further\n\n\n@router.get('/sprints', response_model=list[Sprint])\nasync def get_sprints():\n    sprints = await graphql_api_provider.query_sprints() # read from graphql res.data\n    sprints = [Sprint.model_validate(s) for s in sprints]\n    return await Resolver().resolve(sprints)\n```\n\n## Discussion on Resolver Pattern Design Philosophy\n\nThe core of Resolver pattern is designing data structures based on business requirements.\n\nWhen we remove all resolver and post methods, what remains is the business objects we want to define.\n\nThese methods are just instructions on how to obtain/calculate this data.\n\n**Data structure is the most important asset**, acquisition methods can be freely replaced/optimized.\n\n```python\nclass SimpleStory(BaseModel):  # how to pick fields..\n    id: int\n    name: str\n    point: int\n\n    tasks: list[BaseTask]\n    done_perc: float\n\nclass Sprint(BaseSprint):\n\n    simple_stories: list[SimpleStory]\n    task_count: int\n```\n\nLet's recap the design process from the beginning:\n\nThrough ER models, we can define relationships between data, which are the \"constraints\" for all data combinations. For example, Sprint -\u003e Story follows a 1:N relationship.\n\nTherefore, we can add stories field to Sprint.\n\nBy adding default values, we allow this object to ignore missing values during initialization because data will be set in subsequent processing. This processing might happen in resolver or post.\n\n\u003e In other words, if your initialization data already contains tasks data, then `tasks: list[BaseTask]` doesn't need to set `[]` default value. Remember pydantic supports loading nested data.\n\n```python\nclass SimpleStory(BaseModel):  # how to pick fields..\n    id: int\n    name: str\n    point: int\n\n    tasks: list[BaseTask] = []\n    done_perc: float = 0\n\nclass Sprint(BaseSprint):\n    simple_stories: list[SimpleStory] = []\n```\n\nThen set resolver methods for these values to be queried:\n\n```python\nclass SimpleStory(BaseModel):  # how to pick fields..\n    id: int\n    name: str\n    point: int\n\n    tasks: list[BaseTask] = []\n    def resolve_tasks(self, loader=LoaderDepend(TaskLoader)):\n        return loader.load(self.id)\n\n    done_perc: float = 0\n\nclass Sprint(BaseSprint):\n    simple_stories: list[SimpleStory] = []\n    def resolve_simple_stories(self, loader=LoaderDepend(StoryLoader)):\n        return loader.load(self.id)\n```\n\nSome values need to wait for all tasks data to be fetched before being calculated, so they need to be set through post methods:\n\n```python\n@ensure_subset(BaseStory)\nclass SimpleStory(BaseModel):  # how to pick fields..\n    __pydantic_resolve_collect__ = {'tasks': ('task_count', 'task_count2')}  # send tasks to collectors\n\n    id: int\n    name: str\n    point: int\n    tasks: list[BaseTask]\n\n    done_perc: float = 0.0\n    def post_done_perc(self):\n        # self.tasks is filled with real values\n        if self.tasks:\n            done_count = sum(1 for task in self.tasks if task.done)\n            return done_count / len(self.tasks) * 100\n        else:\n            return 0\n\nclass Sprint(BaseSprint):\n    simple_stories: list[SimpleStory]\n    task_count: int = 0\n    def post_task_count(self, collector=Collector(alias='task_count', flat=True)):\n        return len(collector.values())  # this can be optimized further\n```\n\n\u003e If there are values that need to be calculated based on post calculation outputs, pydantic-resolve provides `post_default_handler` to handle this.\n\n**Therefore, pydantic objects define the expected data structure (interface design), while resolver and post methods just provide specific implementation methods.**\n\n\u003e The reason for recommending Dataloader is that it balances query complexity and runtime efficiency best. But as mentioned earlier, when there are better/faster ways to obtain associated data (like optimized ORM queries), we can immediately complete code refactoring by just removing resolver and Dataloader.\n\u003e Pydantic can load nested objects, so there's no need to limit yourself to returning flat object data in resolvers. (Nested dicts are ok too)\n\n## Resolver vs GraphQL Pattern Comparison\n\n| Feature               | Resolver Pattern                                       | GraphQL Pattern                                                 |\n| --------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |\n| Interface Design      | Based on URL paths and HTTP methods                    | Based on single endpoint and typed Schema                       |\n| Data Fetching         | Separate interfaces, internal code static construction | Single request can fetch multiple resources, on-demand querying |\n| Flexibility           | Fixed return structure, can flexibly define fields     | Frontend can customize query fields, higher flexibility         |\n| Documentation \u0026 Types | Swagger/OpenAPI3.0, supports SDK generation            | Auto-generated Playground, strong type validation               |\n\nThis project implements both Resolver and GraphQL interfaces for comparison and learning the usage and pros/cons of both.\n\n### GraphQL\n\nFlexible, queryable, suitable for scenarios requiring flexible data querying\n\n![image](https://github.com/user-attachments/assets/cf80c282-b3bc-472d-a584-bbb73a213d4d)\n\n### Resolver\n\nUses [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)\n\nUses fewer technology stacks to build equivalent data structures, suitable for internal API integration scenarios\n\nCan use tools like https://github.com/hey-api/openapi-ts to generate frontend SDKs\n\n![image](https://github.com/user-attachments/assets/bb922804-5ed8-429c-b907-a92bf3c4b3ed)\n\n## Benchmark\n\nFinally, using Resolver pattern doesn't affect interface performance and can actually become faster.\n\nYou can easily refactor GraphQL code using Resolver, and this process won't have too much mental burden but will actually streamline various codes.\n\nTherefore, for **internal frontend-backend API integration** scenarios, Resolver pattern is a reliable choice.\n\nin bench, we also include dataclass.\n\n\n```\nuvicorn app_bench.main:app \n```\n\n`ab -c 50 -n 1000`\n\n\n### Resolver\n\npydantic: 418 req/sec\n\n```shell\nConcurrency Level:      50\nTime taken for tests:   2.390 seconds\nComplete requests:      1000\nFailed requests:        0\nTotal transferred:      5078000 bytes\nHTML transferred:       4951000 bytes\nRequests per second:    418.34 [#/sec] (mean)\nTime per request:       119.521 [ms] (mean)\nTime per request:       2.390 [ms] (mean, across all concurrent requests)\nTransfer rate:          2074.53 [Kbytes/sec] received\n\nConnection Times (ms)\n              min  mean[+/-sd] median   max\nConnect:        0    0   0.3      0       1\nProcessing:    30  116  10.4    116     149\nWaiting:       29  115  10.4    114     149\nTotal:         31  116  10.4    116     150\n\nPercentage of the requests served within a certain time (ms)\n  50%    116\n  66%    120\n  75%    123\n  80%    124\n  90%    128\n  95%    131\n  98%    141\n  99%    148\n 100%    150 (longest request)\n```\n\ndataclass: 499 req/sec\n\n```shell\nConcurrency Level:      50\nTime taken for tests:   2.001 seconds\nComplete requests:      1000\nFailed requests:        0\nTotal transferred:      5078000 bytes\nHTML transferred:       4951000 bytes\nRequests per second:    499.70 [#/sec] (mean)\nTime per request:       100.060 [ms] (mean)\nTime per request:       2.001 [ms] (mean, across all concurrent requests)\nTransfer rate:          2478.01 [Kbytes/sec] received\n\nConnection Times (ms)\n              min  mean[+/-sd] median   max\nConnect:        0    0   0.2      0       1\nProcessing:    25   98  10.4     95     125\nWaiting:       25   96   9.7     94     125\nTotal:         25   98  10.4     96     125\n\nPercentage of the requests served within a certain time (ms)\n  50%     96\n  66%    101\n  75%    105\n  80%    107\n  90%    112\n  95%    116\n  98%    119\n  99%    123\n 100%    125 (longest request)\n------------ graphql------------\nThis is ApacheBench, Version 2.3 \u003c$Revision: 1913912 $\u003e\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/\nLicensed to The Apache Software Foundation, http://www.apache.org/\n\nBenchmarking localhost (be patient)\nCompleted 100 requests\nCompleted 200 requests\nCompleted 300 requests\nCompleted 400 requests\nCompleted 500 requests\nCompleted 600 requests\nCompleted 700 requests\nCompleted 800 requests\nCompleted 900 requests\nCompleted 1000 requests\nFinished 1000 requests\n```\n\n### GraphQL\n\nstrawberry: 289 req/sec\n\n```shell\nServer Software:        uvicorn\nServer Hostname:        localhost\nServer Port:            8000\n\nDocument Path:          /graphql\nDocument Length:        4883 bytes\n\nConcurrency Level:      50\nTime taken for tests:   3.453 seconds\nComplete requests:      1000\nFailed requests:        0\nTotal transferred:      5010000 bytes\nTotal body sent:        382000\nHTML transferred:       4883000 bytes\nRequests per second:    289.59 [#/sec] (mean)\nTime per request:       172.656 [ms] (mean)\nTime per request:       3.453 [ms] (mean, across all concurrent requests)\nTransfer rate:          1416.86 [Kbytes/sec] received\n                        108.03 kb/s sent\n                        1524.89 kb/s total\n\nConnection Times (ms)\n              min  mean[+/-sd] median   max\nConnect:        0    0   0.2      0       1\nProcessing:    27  169  14.0    171     242\nWaiting:       26  167  13.9    168     240\nTotal:         27  169  14.0    171     243\n\nPercentage of the requests served within a certain time (ms)\n  50%    171\n  66%    173\n  75%    174\n  80%    175\n  90%    177\n  95%    180\n  98%    194\n  99%    216\n 100%    243 (longest request)\n ```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fallmonday%2Fresolver-vs-graphql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fallmonday%2Fresolver-vs-graphql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fallmonday%2Fresolver-vs-graphql/lists"}