{"id":19866680,"url":"https://github.com/vt-elixir/ja_resource","last_synced_at":"2025-09-09T22:42:30.404Z","repository":{"id":57508996,"uuid":"49006860","full_name":"vt-elixir/ja_resource","owner":"vt-elixir","description":"A behaviour to reduce boilerplate code in your JSON-API compliant Phoenix controllers without sacrificing flexibility.","archived":false,"fork":false,"pushed_at":"2020-01-24T12:46:42.000Z","size":117,"stargazers_count":113,"open_issues_count":11,"forks_count":33,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-30T03:08:48.052Z","etag":null,"topics":["elixir","json-api","phoenix"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vt-elixir.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}},"created_at":"2016-01-04T15:52:27.000Z","updated_at":"2024-07-17T04:41:25.000Z","dependencies_parsed_at":"2022-08-30T07:10:10.396Z","dependency_job_id":null,"html_url":"https://github.com/vt-elixir/ja_resource","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vt-elixir%2Fja_resource","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vt-elixir%2Fja_resource/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vt-elixir%2Fja_resource/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vt-elixir%2Fja_resource/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vt-elixir","download_url":"https://codeload.github.com/vt-elixir/ja_resource/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247430963,"owners_count":20937875,"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":["elixir","json-api","phoenix"],"created_at":"2024-11-12T15:26:54.626Z","updated_at":"2025-04-06T04:15:10.118Z","avatar_url":"https://github.com/vt-elixir.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JaResource\n\n[![Build Status](https://travis-ci.org/vt-elixir/ja_resource.svg?branch=master)](https://travis-ci.org/vt-elixir/ja_resource)\n[![Hex Version](https://img.shields.io/hexpm/v/ja_resource.svg)](https://hex.pm/packages/ja_resource)\n\nA behaviour to reduce boilerplate code in your JSON-API compliant Phoenix\ncontrollers without sacrificing flexibility.\n\nExposing a resource becomes as simple as:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource # or add to web/web.ex\n  plug JaResource\nend\n```\n\nJaResource intercepts requests for index, show, create, update, and delete\nactions and dispatches them through behaviour callbacks. Most resources need\nonly customize a few callbacks. It is a webmachine like approach to building\nAPIs on top of Phoenix.\n\nJaResource is built to work in conjunction with sister library\n[JaSerializer](https://github.com/vt-elixir/ja_serializer). JaResource\nhandles the controller side of things while JaSerializer is focused exclusively\non view logic.\n\nSee [Usage](#usage) for more details on customizing and restricting endpoints.\n\n## Rationale\n\nJaResource lets you focus on the data in your APIs, instead of worrying about\nresponse status, rendering validation errors, and inserting changesets. You get\nrobust patterns and while reducing maintenance overhead.\n\nAt Agilion we value moving quickly while developing quality applications. This\nlibrary has come out of our experience building many APIs in a variety of\nfields.\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed by:\n\n  1. Adding ja_resource to your list of dependencies in `mix.exs`:\n\n        def deps do\n          [{:ja_resource, \"~\u003e 0.1.0\"}]\n        end\n\n  2. Ensuring ja_resource is started before your application:\n\n        def application do\n          [applications: [:ja_resource]]\n        end\n\n  3. ja_resource can be configured to execute queries on a given repo.  While not required, we encourage doing so to preserve clarity:\n\n        config :ja_resource,\n          repo: MyApp.Repo\n\n  4. JaSerializer / JSON-API setup. JaResource is built to work with JaSerializer. Please refer to https://github.com/vt-elixir/ja_serializer#phoenix-usage to setup Plug and Phoenix for JaSerializer and JaResource.\n\n\n## Usage\n\nFor the most simplistic resources JaSerializer lets you replace hundreds of\nlines of boilerplate with a simple use and plug statements.\n\nThe JaResource plug intercepts requests for standard actions and queries,\nfilters, create changesets, applies changesets, responds appropriately and\nmore all for you.\n\nCustomizing each action just becomes implementing the callback relevant to\nwhat functionality you want to change.\n\nTo expose index, show, update, create, and delete of the `MyApp.Post` model\nwith no restrictions:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource # Optionally put in web/web.ex\n  plug JaResource\nend\n```\n\nYou can optionally prevent JaResource from intercepting actions completely as\nneeded:\n\n```elixir\ndefmodule MyApp.V1.PostsController do\n  use MyApp.Web, :controller\n  use JaResource\n  plug JaResource, except: [:delete]\n\n  # Standard Phoenix Delete\n  def delete(conn, params) do\n    # Custom delete logic\n  end\nend\n```\n\nAnd because JaResource is just implementing actions, you can still use plug\nfilters just like in normal Phoenix controllers, however you will want to\ncall the JaResource plug last.\n\n```elixir\ndefmodule MyApp.V1.PostsController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  plug MyApp.Authenticate when action in [:create, :update, :delete]\n  plug JaResource\nend\n```\n\nYou are also free to define any custom actions in your controller, JaResource\nwill not interfere with them at all.\n\n```elixir\ndefmodule MyApp.V1.PostsController do\n  use MyApp.Web, :controller\n  use JaResource\n  plug JaResource\n\n  def publish(conn, params) do\n   # Custom action logic\n  end\nend\n```\n\n### Changing the model exposed\n\nBy default JaResource parses the controller name to determine the model exposed\nby the controller. `MyApp.UserController` will expose the `MyApp.User` model,\n`MyApp.API.V1.CommentController` will expose the `MyApp.Comment` model.\n\nThis can easily be overridden by defining the `model/0` callback:\n\n```elixir\ndefmodule MyApp.V1.PostsController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def model, do: MyApp.Models.BlogPost\nend\n```\n\n### Customizing records returned\n\nMany applications need to expose only subsets of a resource to a given user,\nthose they have access to or maybe just models that are not soft deleted.\nJaResource allows you to define the `records/1` and `record/2`\n\n`records/1` is used by index, show, update, and delete requests to get the base\nquery of records. Many controllers will override this:\n\n```elixir\ndefmodule MyApp.V1.MyPostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def model, do: MyApp.Post\n  def records(%Plug.Conn{assigns: %{user_id: user_id}}) do\n    model\n    |\u003e where([p], p.author_id == ^user_id)\n  end\nend\n```\n\n`record/2` receives the `conn` and the id param and returns a\nsingle record for use in show, update, and delete.\nThe default implementation calls `records/1` with the `conn`, then narrows the query to find only the record with the expected id.\nThis is less common to customize, but may be useful if using non-id fields in the url:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def record(conn, slug_as_id) do\n    conn\n    |\u003e records\n    |\u003e MyApp.Repo.get_by(slug: slug_as_id)\n  end\nend\n```\n\n### 'Handle' Actions\n\nEvery action not excluded defines a default `handle_` variant which receives\npre-processed data and is expected to return an Ecto query or record. All of\nthe handle calls may also return a conn (including the result of a render\ncall).\n\nAn example of customizing the index and show actions (instead of customizing\n`records/1` and `record/2`) would look something like this:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def handle_index(conn, _params) do\n    case conn.assigns[:user] do\n      nil -\u003e where(Post, [p], p.is_published == true)\n      u   -\u003e Post # all posts\n    end\n  end\n\n  def handle_show(conn, id) do\n    Repo.get_by(Post, slug: id)\n  end\nend\n```\n\n### Filtering and Sorting\n\nThe handle_index has complimentary callbacks filter/4 and sort/4. These two\ncallbacks are called once for each value in the related param. The filtering\nand sorting is done on the results of your `handle_index/2` callback (which\ndefaults to the results of your `records/1` callback).\n\nFor example, given the following request:\n\n`GET /v1/articles?filter[category]=dogs\u0026filter[favourite-snack]=cheese\u0026sort=-published`\n\nYou would implement the following callbacks:\n\n```elixir\ndefmodule MyApp.ArticleController do\n  use MyApp.Web, :controller\n  use JaSerializer\n\n  def filter(_conn, query, \"category\", category) do\n    where(query, category: ^category)\n  end\n  \n  def filter(_conn, query, \"favourite_snack\", snack) do\n    where(query, favourite_snack: ^favourite_snack)\n  en\n\n  def sort(_conn, query, \"published\", direction) do\n    order_by(query, [{^direction, :inserted_at}])\n  end\nend\n```\n\nNote that in the case of `filter[favourite-snack]` JaResource has already helpfully converted the filter param's name from dasherized to underscore (or from [whatever you configured](https://github.com/vt-elixir/ja_serializer#key-format-for-attribute-relationship-and-query-param) your API to use).\n\n### Paginate\n\nThe handle_index_query/2 can be used to apply query params and render_index/3 to serialize meta tag.\n\nFor example, given the following request:\n\n`GET /v1/articles?page[number]=1\u0026page[size]=10`\n\nYou would implement the following callbacks:\n\n```elixir\ndefmodule MyApp.ArticleController do\n  use MyApp.Web, :controller\n  use JaSerializer\n\n  def handle_index_query(%{query_params: params}, query) do\n    number = String.to_integer(params[\"page\"][\"number\"])\n    size = String.to_integer(params[\"page\"][\"size\"])\n    total = from(t in subquery(query), select: count(\"*\")) |\u003e repo().one()\n\n    records =\n      query\n      |\u003e limit(^(number + 1))\n      |\u003e offset(^(number * size))\n      |\u003e repo().all()\n\n    %{\n      page: %{\n        number: number,\n        size: size\n      },\n      total: total,\n      records: records\n    }\n  end\n\n  def render_index(conn, paginated, opts) do\n    conn\n    |\u003e Phoenix.Controller.render(\n      :index,\n      data: paginated.records,\n      opts: opts ++ [\n        meta: %{\n          page: paginated.page,\n          total: paginated.total\n        }\n      ]\n    )\n  end\nend\n```\n\n### Creating and Updating\n\nLike index and show, customizing creating and updating resources can be done\nwith the `handle_create/2` and `handle_update/3` actions, however if just\ncustomizing what attributes to use, prefer `permitted_attributes/3`.\n\nFor example:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def permitted_attributes(conn, attrs, :create) do\n    attrs\n    |\u003e Map.take(~w(title body type category_id))\n    |\u003e Map.merge(\"author_id\", conn.assigns[:current_user])\n  end\n\n  def permitted_attributes(_conn, attrs, :update) do\n    Map.take(attrs, ~w(title body type category_id))\n  end\nend\n```\n\nNote: The attributes map passed into `permitted_attributes` is a \"flattened\"\nversion including the values at `data/attributes`, `data/type` and any\nrelationship values in `data/relationships/[name]/data/id` as `name_id`.\n\n#### Create\n\nCustomizing creation can be done with the `handle_create/2` function.\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def handle_create(conn, attributes) do\n    Post.publish_changeset(%Post{}, attributes)\n  end\nend\n```\n\nThe attributes argument is the result of the `permitted_attributes` function.\n\nIf this function returns a changeset it will be inserted and errors rendered if\nrequired. It may also return a model or validation errors for rendering\nor a %Plug.Conn{} for total rendering control.\n\nBy default this will call `changeset/2` on the model defined by `model/0`.\n\n#### Update\n\nCustomizing update can be done with the `handle_update/3` function.\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def handle_update(conn, post, attributes) do\n    current_user_id = conn.assigns[:current_user].id\n    case post.author_id do\n      ^current_user_id -\u003e {:error, author_id: \"you can only edit your own posts\"}\n      _                -\u003e Post.changeset(post, attributes, :update)\n    end\n  end\nend\n```\n\nIf this function returns a changeset it will be inserted and errors rendered if\nrequired. It may also return a model or validation errors for rendering\nor a %Plug.Conn{} for total rendering control.\n\nThe record argument (`post` in the above example) is the record found by the\n`record/3` callback. If `record/3` can not find a record it will be nil.\n\nThe attributes argument is the result of the `permitted_attributes` function.\n\nBy default this will call `changeset/2` on the model defined by `model/0`.\n\n#### Delete\n\nCustomizing delete can be done with the `handle_delete/2` function.\n\n```elixir\ndef handle_delete(conn, post) do\n  case conn.assigns[:user] do\n    %{is_admin: true} -\u003e super(conn, post)\n    _                 -\u003e send_resp(conn, 401, \"nope\")\n  end\nend\n```\n\nThe record argument (`post` in the above example) is the record found by the\n`record/2` callback. If `record/2` can not find a record it will be nil.\n\n### Custom responses\n\nIt is possible to override the default responses for create and update actions\nin both the success and invalid cases.\n\n#### Create\n\nCustomizing the create response can be done with the `render_create/2` and\n`handle_invalid_create/2` functions. For example:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def render_create(conn, model) do\n    conn\n    |\u003e Plug.Conn.put_status(:ok)\n    |\u003e Phoenix.Controller.render(:show, data: model)\n  end\n\n  def handle_invalid_create(conn, errors),\n    conn\n    |\u003e Plug.Conn.put_status(401)\n    |\u003e Phoenix.Controller.render(:errors, data: errors)\n  end\nend\n```\n\n### Update\n\nCustomizing the update response can be done with the `render_update/2` and\n`handle_invalid_update/2` functions. For example:\n\n```elixir\ndefmodule MyApp.V1.PostController do\n  use MyApp.Web, :controller\n  use JaResource\n\n  def render_update(conn, model) do\n    conn\n    |\u003e Plug.Conn.put_status(:created)\n    |\u003e Phoenix.Controller.render(:show, data: model)\n  end\n\n  def handle_invalid_update(conn, errors) do\n    conn\n    |\u003e Plug.Conn.put_status(401)\n    |\u003e Phoenix.Controller.render(:errors, data: errors)\n  end\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvt-elixir%2Fja_resource","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvt-elixir%2Fja_resource","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvt-elixir%2Fja_resource/lists"}