{"id":22505179,"url":"https://github.com/azer/carve","last_synced_at":"2025-06-25T10:39:13.137Z","repository":{"id":257815314,"uuid":"869570284","full_name":"azer/carve","owner":"azer","description":"DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.","archived":false,"fork":false,"pushed_at":"2024-12-20T16:08:47.000Z","size":101,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-07T02:49:08.121Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/carve","language":"Elixir","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/azer.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}},"created_at":"2024-10-08T14:14:04.000Z","updated_at":"2024-12-28T19:41:01.000Z","dependencies_parsed_at":"2024-11-24T11:31:31.085Z","dependency_job_id":null,"html_url":"https://github.com/azer/carve","commit_stats":null,"previous_names":["azer/carve"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/azer/carve","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azer%2Fcarve","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azer%2Fcarve/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azer%2Fcarve/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azer%2Fcarve/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/azer","download_url":"https://codeload.github.com/azer/carve/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azer%2Fcarve/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261853337,"owners_count":23219845,"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":[],"created_at":"2024-12-07T00:15:40.413Z","updated_at":"2025-06-25T10:39:13.114Z","avatar_url":"https://github.com/azer.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Carve\n\nDeclare the view \u0026 relationships like this:\n\n```ex\nuse Carve.View, :user\n\nview fn user -\u003e\n  %{\n    id: hash(user.id),\n    name: user.name\n  }\nend\n\nlinks fn user -\u003e\n  %{\n    TeamJSON =\u003e user.team_id,\n    ProfileJSON =\u003e user.profile_id,\n    PostsJSON =\u003e fn -\u003e Posts.get_by_user_id(user.id) end\n   }\nend\n```\n\nGet JSON endpoint views (`index`, `show`) with complete \u0026 linked data back in single request:\n\n```js\n// GET /users/xyz?include=profile,team\n{\n  \"result\": {\n    \"id\": \"D3Wcorr0oa\",\n    \"type\": \"user\",\n    \"data\": { ... }\n  },\n  \"links\": [\n    {\n      \"id\": \"Xk9Lp2Rr4m\",\n      \"type\": \"team\",\n      \"data\": { ... }\n    },\n    {\n      \"id\": \"Bz7Jt5Yq1n\",\n      \"type\": \"profile\",\n      \"data\": { ... }\n    }\n  ]\n}\n```\n\nFeatures:\n\n* Automatic endpoint generation (index/show)\n* Smart data loading (lazy/eager)\n* Relationship resolution\n* Response structuring\n* ID hashing (123 → Xk9Lp2R)\n\nSee also: [Bind](https://github.com/azer/bind)\n\n## Installation\n\nAdd `carve` to your list of dependencies in `mix.exs`:\n\n\n```elixir\ndef deps do\n  [\n    {:carve, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\n\nTo configure custom hashing salt, add to `config.exs`:\n\n```elixir\nconfig :carve,\n  salt: \"your-secret-salt\",\n  min_length: 10\n```\n\n## Usage\n\nIn your Phoenix JSON view, just declare how your endpoint should look like:\n\n```elixir\ndefmodule UserJSON do\n    use Carve.View, :user\n\n    view fn user -\u003e\n      %{\n        id: hash(user.id), # provided by Carve.View, encodes numerical ID (123) with an entity-specific salt (D3Wcorr0oa).\n        name: user.name\n      }\n    end\nend\n```\n\nAnd that's it -- this macro will make `index(%{ result: users })` and `show(%{ result: user })` methods available for your controller.\n\nOf course, just formatting the view alone is not Carvers only point; you can declare the links of each view, and Carve will automatically output them for the client:\n\n```elixir\ndefmodule UserJSON do\n    use Carve.View, :user\n\n    # Return the links of a given user as ViewModule =\u003e id\n    links fn user -\u003e\n        %{\n            FooWeb.TeamJSON =\u003e user.team_id, # You can also pass list of ids or the Ecto record(s)\n            FooWeb.ProfileJSON =\u003e user.profile_id\n        }\n    end\n\n    # Provide a method to retrieve user by id. This will be used by Carve to render links automatically.\n    get fn id -\u003e\n        Foo.Users.get_by_id!(id)\n    end\n\n    view fn user -\u003e\n      %{\n        id: hash(user.id),\n        team_id: FooWeb.TeamJSON.hash(user.team_id),\n        profile_id: FooWeb.ProfileJSON.hash(user.profile_id),\n        name: user.name\n      }\n    end\nend\n```\n\nAfter activating `show` and `index` methods in the controller:\n\n```elixir\nrender(conn, :index, result: requests) # or render(conn, :show, result: record)\n```\n\nExample response Carve will render for this view:\n\n```json\n{\n  \"result\": {\n    \"id\": \"D3Wcorr0oa\",\n    \"type\": \"user\",\n    \"data\": {\n      \"id\": \"D3Wcorr0oa\",\n      \"name\": \"John Doe\",\n      \"team_id\": \"Xk9Lp2Rr4m\",\n      \"profile_id\": \"Bz7Jt5Yq1n\"\n    }\n  },\n  \"links\": [\n    {\n      \"id\": \"Xk9Lp2Rr4m\",\n      \"type\": \"team\",\n      \"data\": {\n        \"id\": \"Xk9Lp2Rr4m\",\n        \"name\": \"Engineering Team\",\n        \"description\": \"Our awesome engineering team\"\n      }\n    },\n    {\n      \"id\": \"Bz7Jt5Yq1n\",\n      \"type\": \"profile\",\n      \"data\": {\n        \"id\": \"Bz7Jt5Yq1n\",\n        \"bio\": \"Software engineer passionate about Elixir\",\n        \"avatar_url\": \"https://example.com/avatars/johndoe.jpg\"\n      }\n    }\n  ]\n}\n```\n\n### Example Controller\n\nCarve provides a flexible way to control which linked data is included in the response. This is achieved through the include parameter. Here's an example of how to use Carve in your Phoenix controllers:\n\n```elixir\ndefmodule UserController do\n  use FooWeb, :controller\n\n  def show(conn, %{\"id\" =\u003e id} = params) do\n    user = Foo.Users.get_user!(id)\n    include = Carve.parse_include(params)\n\n    render(conn, :show, %{ result: user, include: include })\n  end\n\n  def index(conn, params) do\n    users = Foo.Users.list_users()\n    include = Carve.parse_include(params)\n\n    render(conn, :index, %{ result: users, include: include })\n  end\nend\n```\n\nThis example also shows reading \u0026 parsing the `include` parameter, which can be one of following:\n\n* Not specified (`GET  /api/users`): All link types are included.\n* Empty list (`GET /api/users?included=`): No link types are included.\n* Custom types: (`GET /api/users/123?include=team,profile`): Include comma-separated link types only.\n\n\n## Links\n\nCarve allows you to define links between resources directly in the view. When a user fetches a resource, all necessary context is automatically included in the response:\n\n```elixir\ndefmodule UserJSON do\n  use Carve.View, :user\n\n  links fn user -\u003e\n    %{\n      TeamJSON =\u003e user.team_id,\n      CompanyJSON =\u003e user.company_id\n    }\n  end\nend\n```\n\nNow, a request to `/api/users/123` automatically includes linked team and company in a single response. Client gets all data needed without extra requests.\n\nUnlike GraphQL which requires defining a schema and writing resolvers for each field, Carve allows you to define links between resources directly in the view. When a user fetches a resource, all necessary context is automatically included in the response:\n\n```elixir\ndefmodule UserJSON do\n  use Carve.View, :user\n\n  # Declare which other resources this view links to\n  links fn user -\u003e\n    %{\n      TeamJSON =\u003e user.team_id,       # User's team - needed to render user profile\n      CompanyJSON =\u003e user.company_id  # User's company - needed for permissions\n    }\n  end\nend\n```\n\n### Include Parameter\n\nCarve allows selective loading so the API client can optimize the response size \u0026 number of DB queries.\n\nYou can enable it in the controller:\n\n```ex\ndef show(conn, params) do\n  user = Users.get!(params[\"id\"])\n\n  # Parses ?include=team,post into [:team, :post]\n  include = Carve.parse_include(params)\n\n  render(conn, :show, %{\n    result: user,\n    include: include\n  })\nend\n```\n\nThe client now can specify what links should be included in the API.\n\n```\nGET /api/users/123                    # Include all links\nGET /api/users/123?include=           # Include no links\nGET /api/users/123?include=team       # Include only team links\nGET /api/users/123?include=team,post  # Include team and post links\n```\n\nExample response with `?include=team`:\n\n```json\n{\n  \"result\": {\n    \"id\": \"D3Wcorr0oa\",\n    \"type\": \"user\",\n    \"data\": {\n      \"id\": \"D3Wcorr0oa\",\n      \"name\": \"John Doe\",\n      \"team_id\": \"Xk9Lp2Rr4m\"\n    }\n  },\n  \"links\": [\n    {\n      \"id\": \"Xk9Lp2Rr4m\",\n      \"type\": \"team\",\n      \"data\": {\n        \"id\": \"Xk9Lp2Rr4m\",\n        \"name\": \"Engineering Team\"\n      }\n    }\n  ]\n}\n```\n\n### Lazy Links\n\nWhen using `links`, even if a link type is filtered out via `?include=`, database queries are still executed for all linked resources.\n\nFor expensive queries, you can simply declare lazy links;\n\n```elixir\ndefmodule UserJSON do\n  use Carve.View, :user\n\n  links fn user -\u003e\n    %{\n      TeamJSON =\u003e user.team_id, # Included by default\n      CommentJSON =\u003e fn -\u003e Comments.by_user_id(user.id) end # Called \u0026 included only if specified explicitly\n    }\n  end\nend\n```\n\nThe lazy function is evaluated only when requested in the include param:\n\n```\nGET /api/users/123                # No comments query executed\nGET /api/users/123?include=comments  # Query executed for fetching comments\n```\n\nBoth return same JSON format, just with different loading behavior. The function should return a tuple of `{ViewModule, id_or_ids}`.\n\n### Large datasets\n\nFor relationships that could return large datasets, create dedicated endpoints instead of links:\n\n```elixir\n#  ✅ Good: /api/users/123 returns user with essential context\nlinks fn user -\u003e\n  %{TeamJSON =\u003e user.team_id}\nend\n\n# ✅ Good: Get user's comments via dedicated endpoint\nGET /api/users/123/comments?page=1\n\n# ❌ Bad: Don't use links for large collections\nlinks fn user -\u003e\n  %{\n    CommentsJSON =\u003e Comments.by_user_id(user.id)  # Could be thousands\n  }\nend\n```\n\n## How does it work?\n\n* Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })`\n* Controller calls these view functions\n* Carve pulls the list of links for given data (list or single record)\n* Carve calls the `get_by_id` (`get` macro expanded) and `prepare_for_view` (`view` macro expanded) functions for each link\n* The final expanded list of links get flattened \u0026 cleaned, returned to user with the main result: `{ result: {} || [], links: [] }`\n\n\n## API\n\nMore detailed API docs are available at [https://hexdocs.pm/carve/Carve.html](https://hexdocs.pm/carve/Carve.html)\n\n## Q\u0026A\n\n### Why?\n\n1. **Performance vs Flexibility**\n   - REST: Simple but requires multiple roundtrips\n   - GraphQL: Flexible but complex infrastructure\n   - Carve: Single request, zero infrastructure\n\n2. **Development Speed**\n   - Every new feature typically requires:\n     - New endpoint handlers\n     - New view logic\n     - New data fetching code\n     - New state management\n   - Carve eliminates all of this with one view definition\n\n3. **Maintainability**\n   - As applications grow, data dependencies become complex\n   - Changes ripple through multiple endpoints\n   - State management becomes increasingly difficult\n   - Carve centralizes this complexity in view definitions\n\n### How does it compare to GraphQL?\n\nGraphQL\n- Requires schema definition (IDL) and resolver functions for each field\n- Client needs to learn query language and construct queries\n- Each query is a unique POST request, making caching challenging\n- N+1 query problems require manual batching/dataloader setup\n- Field-level authorization adds complexity\n- Additional infrastructure needed (query validation, complexity limits, persisted queries)\n\nCarve\n- Simple \u0026 small lbirary, uses your existing Phoenix views and controllers\n- Compatible with other libraries like [Bind](https://github.com/azer/bind)\n- REST-like URLs with simple `?include=` parameter\n- Standard GET requests, works with HTTP caching out of the box\n- Automatic batching of related data queries\n- Resource-level authorization using your existing Phoenix plugs\n- Zero additional infrastructure required\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazer%2Fcarve","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fazer%2Fcarve","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazer%2Fcarve/lists"}