{"id":32170779,"url":"https://github.com/ohhi-vn/phoenix_gen_api","last_synced_at":"2026-04-10T06:21:29.765Z","repository":{"id":281101116,"uuid":"943872979","full_name":"ohhi-vn/phoenix_gen_api","owner":"ohhi-vn","description":"General api by using Phoenix Channel","archived":false,"fork":false,"pushed_at":"2026-01-13T10:18:04.000Z","size":100,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-13T13:19:21.177Z","etag":null,"topics":["disport","hex-package","public"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ohhi-vn.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-03-06T12:05:06.000Z","updated_at":"2026-01-13T10:16:21.000Z","dependencies_parsed_at":null,"dependency_job_id":"210e7121-a699-496e-96b1-4ca1d1c55498","html_url":"https://github.com/ohhi-vn/phoenix_gen_api","commit_stats":null,"previous_names":["ohhi-vn/phoenix_gen_api"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/ohhi-vn/phoenix_gen_api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohhi-vn%2Fphoenix_gen_api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohhi-vn%2Fphoenix_gen_api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohhi-vn%2Fphoenix_gen_api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohhi-vn%2Fphoenix_gen_api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ohhi-vn","download_url":"https://codeload.github.com/ohhi-vn/phoenix_gen_api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohhi-vn%2Fphoenix_gen_api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28477792,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["disport","hex-package","public"],"created_at":"2025-10-21T17:24:25.795Z","updated_at":"2026-04-10T06:21:29.746Z","avatar_url":"https://github.com/ohhi-vn.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Docs](https://img.shields.io/badge/api-docs-green.svg?style=flat)](https://hexdocs.pm/phoenix_gen_api)\n[![Hex.pm](https://img.shields.io/hexpm/v/phoenix_gen_api.svg?style=flat\u0026color=blue)](https://hex.pm/packages/phoenix_gen_api)\n\n# PhoenixGenApi\n\nThe library helps quickly develop APIs for client, the library is based on Phoenix Channel.\nDevelopers can add or update APIs in runtime from other nodes in the cluster without restarting or reconfiguring the Phoenix app.\nIn this case, the Phoenix app will take on the role of an API gateway.\n\nThe library can use with [EasyRpc](https://hex.pm/packages/easy_rpc) and [ClusterHelper](https://hex.pm/packages/cluster_helper) for fast and easy to develop a dynamic Elixir cluster.\n\n## Concept\n\nAfter received an event from client(in handle_in callback of Phoenix Channel), the event will be passed to PhoenixGenApi to find target API \u0026 target node to execute then get result for response to client.\n\nFor service nodes (target node), the libray support some basic strategy for selecting node (:choose_node_mode) like: :random, :hash, :round_robin.\n\nSupported :sync, :async, :stream for request/response to client.\n\nSupported basic check type \u0026 permission.\n\n## Features\n\n- **Dynamic Configuration**: Add/update APIs at runtime from remote nodes\n- **Multi-Version Support**: Manage multiple API versions with enable/disable controls\n- **Rate Limiting**: Sliding window rate limiter with global and per-API limits\n- **Permission System**: Flexible authentication and authorization modes\n- **Worker Pools**: Dedicated pools for async and stream operations\n- **Node Selection**: Random, hash-based, and round-robin strategies\n- **Retry on failure**: Configurable retry with same-node or all-nodes strategies\n- **Response Types**: Sync, async, stream, and fire-and-forget\n\n## Installation\n\nNote: Require Elixir ~\u003e 1.18 and OTP ~\u003e 27\n\nThe package can be installed\nby adding `phoenix_gen_api` to your list of dependencies in `mix.exs`:\n\n```Elixir\ndef deps do\n  [\n    {:phoenix_gen_api, \"~\u003e 2.1\"}\n  ]\nend\n```\n\nNote: You can use [`:libcluster`](https://hex.pm/packages/libcluster) to build a Elixir cluster.\n\n## Usage\n\n### Remote Node (optional)\n\nAdd config to your `config.exs` file to mark this is remote node.\n\n```Elixir\nconfig :phoenix_gen_api, :client_mode, true\n```\n\nDeclare a module for support PhoenixGenApi can pull config.\n\nExample:\n\n```Elixir\ndefmodule MyApp.GenApi.Supporter do\n\n  alias PhoenixGenApi.Structs.FunConfig\n\n  @doc \"\"\"\n  Support for remote pull general api config.\n  \"\"\"\n  def get_config(_arg) do\n    {:ok, my_fun_configs()}\n  end\n\n  @doc \"\"\"\n  Return list of %FunConfig{}.\n  \"\"\"\n  def my_fun_configs() do\n    [\n      %FunConfig{\n        request_type: \"get_data\",\n        service: \"my_service\",\n        nodes: [Node.self()],\n        choose_node_mode: :random,\n        timeout: 5_000,\n        mfa: {MyApp.Interface.Api, :get_data, []},\n        arg_types: %{\"id\" =\u003e :string},\n        response_type: :async,\n        version: \"1.0.0\"\n      }\n    ]\n  end\nend\n```\n\nNote: You can add directly in runtime in gateway node without using client mode.\n\n### Phoenix Node (Gateway node)\n\nAdd config for Phoenix can pull config from remote nodes(above) like:\n\n```Elixir\n# Config for general api.\nconfig :phoenix_gen_api, :gen_api,\n  service_configs: [\n    # service config for pulling general api config.\n    %{\n      # service type\n      service: \"my_service\",\n      # nodes of service in cluster, need to connecto to get config\n      # list of nodes or using MFA like: {ClusterHelper, get_nodes, [:my_api]}\n      nodes: [:\"remote_service@test.local\"], \n      # module to get config\n      module: MyApp.GenApi.Supporter,\n      # function to get config\n      function: :get_config,\n      # args to get config, using for identity or check security.\n      args: [:gateway_1],\n    }\n  ]\n\n# Config for rate limiter.\nconfig :phoenix_gen_api, :rate_limiter,\n  enabled: true,\n  global_limits: [\n    %{key: :user_id, max_requests: 2000, window_ms: 60_000},\n    %{key: :device_id, max_requests: 10000, window_ms: 60_000}\n  ],\n  api_limits: [\n    %{\n      service: \"data_service\",\n      request_type: \"export_data\",\n      key: :user_id,\n      max_requests: 10,\n      window_ms: 60_000\n    }\n  ]\n```\n\nIn Phoenix Channel you can add add this line for apply PhoenixGenApi:\n\n```Elixir\nuse  PhoenixGenApi, event: \"phoenix_gen_api\"\n```\n\nFollow functions will be expanded and reserved for PhoenixGenApi.\n\n```Elixir\nhandle_in(\"phoenix_gen_api\", payload, socket) \nhandle_info({:push, result}, socket)\nhandle_info({:async_call, result = %Response{}}, socket)\nhandle_info({:stream_response, result}, socket)\n```\n\nDefault PhoenixGenApi will overwrite user_id in `Request`.\nDisable this by add option: `override_user_id: false` to `use PhoenixGenApi`\n\nIn this case, if need you can authenticate by using Phoenix framework.\n\nNow you can start your cluster and test!\n\nAfter start Phoenix app, PhoenixGenApi will auto pull config from remote node to serve client.\n\nFor test in Elixir you can use  [`phoenix_client`](https://hex.pm/packages/phoenix_client) to create a connection to Phoenix Channel.\n\nYou can push a event with content like:\n\n```json\n{\n  \"user_id\": \"user_1\",\n  \"device_id\": \"device_1\",\n  \"service\": \"my_service\",\n  \"request_type\": \"get_data\",\n  \"request_id\": \"test_request_1\",\n  \"version\": \"1.0.0\",\n  \"args\": {\n    \"id\": \"test_data_id\"\n  }\n}\n```\n\nResult like:\n\nIf is async/stream call you will receive a message like this:\n\n```json\n{\n  \"async\": true,\n  \"error\": \"\",\n  \"has_more\": false,\n  \"request_id\": \"test_request_1\",\n  \"result\": null,\n  \"success\": true\n}\n```\n\nAfter that is a another message with result:\n\n```json\n{\n  \"async\": false,\n  \"error\": \"\",\n  \"has_more\": false,\n  \"request_id\": \"test_request_1\",\n  \"result\": [\n    {\n      \"id\": \"14e99227-512a-47b6-b6b1-2d4bc29ca13e\",\n      \"name\": \"Hello World!\"\n    }\n  ]\n}\n```\n\nFor better security you can overwrite user_id in server, using basic check permission or passing request info (user_id, device_id, request_id).\n\n## Function Versioning\n\nPhoenixGenApi supports multiple versions of the same API, allowing you to manage API evolution and deprecation gracefully.\n\n### Version Configuration\n\nEach `FunConfig` can have a `version` field (defaults to `\"0.0.0\"` if not specified):\n\n```elixir\n%FunConfig{\n  request_type: \"get_user\",\n  service: \"user_service\",\n  version: \"1.0.0\",\n  nodes: :local,\n  mfa: {MyApp.Users, :get_user_v1, []},\n  arg_types: %{\"id\" =\u003e :string},\n  response_type: :sync\n}\n\n%FunConfig{\n  request_type: \"get_user\",\n  service: \"user_service\",\n  version: \"2.0.0\",\n  nodes: :local,\n  mfa: {MyApp.Users, :get_user_v2, []},\n  arg_types: %{\"id\" =\u003e :string, \"fields\" =\u003e :list_string},\n  response_type: :sync\n}\n```\n\n### Request Version\n\nClients can specify which version they want to use by including the `version` field in their request:\n\n```json\n{\n  \"user_id\": \"user_1\",\n  \"service\": \"user_service\",\n  \"request_type\": \"get_user\",\n  \"request_id\": \"req_1\",\n  \"version\": \"2.0.0\",\n  \"args\": {\n    \"id\": \"123\",\n    \"fields\": [\"name\", \"email\"]\n  }\n}\n```\n\nIf no version is specified, the system defaults to `\"0.0.0\"`.\n\n### Managing Versions\n\nYou can manage API versions programmatically:\n\n```elixir\nalias PhoenixGenApi.ConfigDb\n\n# Get a specific version\n{:ok, config} = ConfigDb.get(\"user_service\", \"get_user\", \"1.0.0\")\n\n# Get the latest enabled version\n{:ok, latest_config} = ConfigDb.get_latest(\"user_service\", \"get_user\")\n\n# Disable a version (e.g., for deprecation)\n:ok = ConfigDb.disable(\"user_service\", \"get_user\", \"1.0.0\")\n\n# Re-enable a disabled version\n:ok = ConfigDb.enable(\"user_service\", \"get_user\", \"1.0.0\")\n\n# Delete a specific version\n:ok = ConfigDb.delete(\"user_service\", \"get_user\", \"1.0.0\")\n\n# List all functions with their versions\n%{\n  \"user_service\" =\u003e %{\n    \"get_user\" =\u003e [\"1.0.0\", \"2.0.0\"],\n    \"create_user\" =\u003e [\"1.0.0\"]\n  }\n} = ConfigDb.get_all_functions()\n```\n\n### Version Behavior\n\n- **Disabled versions** return `{:error, :disabled}` when accessed\n- **Non-existent versions** return `{:error, :not_found}`\n- **Multiple versions** can coexist independently\n- **Disabling one version** does not affect other versions\n- **`get_latest/2`** returns the highest version number that is enabled\n\n## Rate Limiter\n\nPhoenixGenApi includes a high-performance sliding window rate limiter using ETS for tracking.\n\n### Configuration\n\nConfigure rate limits in `config.exs`:\n\n```elixir\nconfig :phoenix_gen_api, :rate_limiter,\n  enabled: true,\n  global_limits: [\n    # 2000 requests per minute per user\n    %{key: :user_id, max_requests: 2000, window_ms: 60_000},\n    # 10000 requests per minute per device\n    %{key: :device_id, max_requests: 10000, window_ms: 60_000}\n  ],\n  api_limits: [\n    # Expensive operation: 10 requests per minute\n    %{\n      service: \"data_service\",\n      request_type: \"export_data\",\n      key: :user_id,\n      max_requests: 10,\n      window_ms: 60_000\n    },\n    # Public endpoint: 100 requests per minute\n    %{\n      service: \"public_service\",\n      request_type: \"search\",\n      key: :ip_address,\n      max_requests: 100,\n      window_ms: 60_000\n    }\n  ]\n```\n\n### Usage\n\nRate limiting is automatically checked when you call `Executor.execute!/1` or `Executor.execute_params!/1`.\n\nYou can also check rate limits manually:\n\n```elixir\nalias PhoenixGenApi.RateLimiter\n\n# Check rate limit for a request\ncase RateLimiter.check_rate_limit(request) do\n  :ok -\u003e\n    # Proceed with execution\n    Executor.execute!(request)\n\n  {:error, :rate_limited, details} -\u003e\n    # Return rate limit error\n    %{\n      error: \"Rate limit exceeded\",\n      retry_after: details.retry_after_ms,\n      current_requests: details.current_requests,\n      max_requests: details.max_requests\n    }\nend\n\n# Check global rate limit directly\nRateLimiter.check_rate_limit(\"user_123\", :global, :user_id)\n\n# Check API-specific rate limit\nRateLimiter.check_rate_limit(\"user_123\", {\"my_service\", \"my_api\"}, :user_id)\n```\n\n### Dynamic Configuration\n\nUpdate rate limits at runtime:\n\n```elixir\n# Add a new global limit\nRateLimiter.add_global_limit(%{\n  key: :ip_address,\n  max_requests: 5000,\n  window_ms: 60_000\n})\n\n# Update configuration\nRateLimiter.update_config(%{\n  enabled: true,\n  global_limits: [...],\n  api_limits: [...]\n})\n```\n\n### Rate Limit Keys\n\nSupported key types:\n- `:user_id` - Rate limit by user\n- `:device_id` - Rate limit by device\n- `:ip_address` - Rate limit by IP address\n- Custom string keys\n\n### Telemetry\n\nThe rate limiter emits telemetry events for monitoring:\n\n```elixir\n:telemetry.attach(\n  \"rate-limiter-monitor\",\n  [:phoenix_gen_api, :rate_limiter, :exceeded],\n  fn event, measurements, metadata, config -\u003e\n    Logger.warning(\"Rate limit exceeded: #{inspect(metadata)}\")\n  end,\n  %{}\n)\n```\n\n## Permission System\n\nPhoenixGenApi provides a flexible permission system with multiple modes for authentication and authorization.\n\n### Permission Modes\n\nConfigure permissions in `FunConfig.check_permission`:\n\n#### 1. Disabled (`false`)\nNo permission check. Useful for public endpoints.\n\n```elixir\n%FunConfig{\n  request_type: \"get_public_data\",\n  check_permission: false\n}\n```\n\n#### 2. Any Authenticated (`:any_authenticated`)\nRequires a valid `user_id`. Any authenticated user can access.\n\n```elixir\n%FunConfig{\n  request_type: \"get_profile\",\n  check_permission: :any_authenticated\n}\n\n# Passes - user is authenticated\nrequest = %Request{user_id: \"user_123\"}\n\n# Fails - no user_id\nrequest = %Request{user_id: nil}\n```\n\n#### 3. Argument-Based (`{:arg, arg_name}`)\nUser can only access their own data. The specified argument must match `user_id`.\n\n```elixir\n%FunConfig{\n  request_type: \"get_user_profile\",\n  check_permission: {:arg, \"user_id\"}\n}\n\n# Passes - user accessing their own data\nrequest = %Request{\n  user_id: \"user_123\",\n  args: %{\"user_id\" =\u003e \"user_123\"}\n}\n\n# Fails - user trying to access another user's data\nrequest = %Request{\n  user_id: \"user_123\",\n  args: %{\"user_id\" =\u003e \"user_999\"}\n}\n```\n\n#### 4. Role-Based (`{:role, allowed_roles}`)\nUser must have one of the specified roles.\n\n```elixir\n%FunConfig{\n  request_type: \"delete_user\",\n  check_permission: {:role, [\"admin\", \"moderator\"]}\n}\n\n# Passes - user has admin role\nrequest = %Request{\n  user_id: \"user_123\",\n  user_roles: [\"admin\"]\n}\n\n# Passes - user has moderator role\nrequest = %Request{\n  user_id: \"user_456\",\n  user_roles: [\"moderator\", \"user\"]\n}\n\n# Fails - user doesn't have required role\nrequest = %Request{\n  user_id: \"user_789\",\n  user_roles: [\"user\"]\n}\n```\n\n### Request Structure for Permissions\n\n```elixir\n%Request{\n  user_id: \"user_123\",           # Required for permission checks\n  user_roles: [\"admin\", \"user\"], # Required for role-based checks\n  request_type: \"get_profile\",\n  service: \"user_service\",\n  request_id: \"req_1\",\n  args: %{\"user_id\" =\u003e \"user_123\"}\n}\n```\n\n### Security Best Practices\n\n- Always use specific permission modes rather than `false` when possible\n- Use `{:arg, \"user_id\"}` to ensure users can only access their own data\n- Use `{:role, [...]}` for admin-only endpoints\n- Missing arguments result in permission denial\n- All permission failures are logged for audit purposes\n- Permission checks happen before argument validation and function execution\n\n## Retry\n\nPhoenixGenApi supports configurable retry behavior when request execution fails. The `retry` field in `FunConfig` controls how failed requests are retried.\n\n### Retry Configuration\n\nThe `retry` field accepts the following values:\n\n| Value | Meaning |\n|-------|---------|\n| `nil` | No retry (default, backward compatible) |\n| `3` (number) | Equivalent to `{:all_nodes, 3}` — retry across all available nodes |\n| `{:same_node, 2}` | Retry on the same node(s) originally selected by `choose_node_mode` |\n| `{:all_nodes, 3}` | Retry across all available nodes in the cluster |\n\n### How It Works\n\nWhen a request execution fails (returns `{:error, _}` or `{:error, _, _}`), the executor retries according to the retry configuration:\n\n- **`{:same_node, n}`**: Retries the request on the same node(s) that were originally selected by the `choose_node_mode` strategy. This is useful when the failure might be transient (e.g., temporary network glitch).\n\n- **`{:all_nodes, n}`**: On each retry, fetches the full list of available nodes and tries all of them. This is useful when a node might be down and you want to try other nodes.\n\n- **Local execution**: For `nodes: :local`, both `:same_node` and `:all_nodes` retry on the same local machine since there's only one node.\n\n### Validation\n\nThe `retry` field is validated as follows:\n\n- `nil` is valid (no retry)\n- A positive number is valid\n- `{:same_node, positive_number}` is valid\n- `{:all_nodes, positive_number}` is valid\n- Zero, negative numbers, strings, and other formats are invalid\n\n### Normalization\n\n`FunConfig.normalize_retry/1` converts the raw config to a standard format:\n\n| Input | Normalized |\n|-------|-----------|\n| `nil` | `nil` |\n| `3` | `{:all_nodes, 3}` |\n| `{:same_node, 2}` | `{:same_node, 2}` |\n| `{:all_nodes, 5}` | `{:all_nodes, 5}` |\n| `2.7` (float) | `{:all_nodes, 2}` (truncated) |\n\n### Telemetry\n\nRetry attempts emit telemetry events at `[:phoenix_gen_api, :executor, :retry]` with:\n\n- **measurements**: `%{attempt: n}` — the remaining retry count\n- **metadata**: `%{mode: :same_node | :all_nodes, type: :local | :remote}`\n\n```elixir\n:telemetry.attach(\n  \"retry-monitor\",\n  [:phoenix_gen_api, :executor, :retry],\n  fn _event, measurements, metadata, _config -\u003e\n    Logger.info(\"Retry attempt #{measurements.attempt}, mode: #{metadata.mode}, type: #{metadata.type}\")\n  end,\n  %{}\n)\n```\n\n### Example FunConfig with Retry\n\n```elixir\n%FunConfig{\n  request_type: \"get_data\",\n  service: \"my_service\",\n  nodes: [:\"node1@host\", :\"node2@host\", :\"node3@host\"],\n  choose_node_mode: :random,\n  timeout: 5_000,\n  mfa: {MyApp.Interface.Api, :get_data, []},\n  arg_types: %{\"id\" =\u003e :string},\n  response_type: :sync,\n  retry: {:all_nodes, 3}  # Retry up to 3 times across all available nodes\n}\n```\n\n## Full Example\n\nWe will add a full example in the future.\n\n## Planned Features\n\n- [DONE] Add pool processes for save/limit resource.\n- [DONE] Function versioning with enable/disable support.\n- [DONE] Rate limiter with sliding window algorithm.\n- Sticky node.\n\n## Support AI agents \u0026 MCP for dev \u0026 improvement\n\nRun this command for update guide \u0026 rules from deps to repo for supporting ai agents.\n\n```bash\nmix usage_rules.sync AGENTS.md --all \\\n  --link-to-folder deps \\\n  --inline usage_rules:all\n```\n\nRun this command for enable MCP server\n\n```bash\nmix tidewave\n```\n\nConfig MCP for agent `http://localhost:4114/tidewave/mcp`, changes port in `mix.exs` file if needed. Go to [Tidewave](https://hexdocs.pm/tidewave/) for more informations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fohhi-vn%2Fphoenix_gen_api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fohhi-vn%2Fphoenix_gen_api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fohhi-vn%2Fphoenix_gen_api/lists"}