documentation/topics/routing.md

<!--
SPDX-FileCopyrightText: 2020 Zach Daniel

SPDX-License-Identifier: MIT
-->

# Routing

AshJsonApi provides a set of route helpers that map HTTP requests to Ash actions. Routes are defined inside the `json_api do routes do ... end end` block on either a resource or a domain.

## Route overview

| Route Helper | HTTP Method | Default Path | Primary Action Type | Also Accepts |
|---|---|---|---|---|
| `get` | GET | `/:id` | `:read` | `:action` |
| `index` | GET | `/` | `:read` | `:action` |
| `post` | POST | `/` | `:create` | `:action`, `:read` |
| `patch` | PATCH | `/:id` | `:update` | `:action` |
| `delete` | DELETE | `/:id` | `:destroy` | `:action` |
| `related` | GET | `/:id/<relationship>` | `:read` | — |
| `relationship` | GET | `/:id/relationships/<relationship>` | `:read` | — |
| `post_to_relationship` | POST | `/:id/relationships/<relationship>` | `:update` | — |
| `patch_relationship` | PATCH | `/:id/relationships/<relationship>` | `:update` | — |
| `delete_from_relationship` | DELETE | `/:id/relationships/<relationship>` | `:update` | — |
| `route` | *any* | *required* | `:action` | — |

## Defining routes

Routes can live on the resource or on the domain. Defining them on the domain is the default recommendation — it keeps resources focused on data and actions while the domain acts as the API surface.

### On the domain

```elixir
defmodule MyApp.Support do
  use Ash.Domain, extensions: [AshJsonApi.Domain]

  json_api do
    routes do
      base_route "/tickets", MyApp.Support.Ticket do
        get :read
        index :read
        post :create
        patch :update
        delete :destroy
      end
    end
  end
end
```

`base_route` scopes all nested routes under the given path prefix for the specified resource.

### On the resource

```elixir
defmodule MyApp.Support.Ticket do
  use Ash.Resource, extensions: [AshJsonApi.Resource]

  json_api do
    type "ticket"

    routes do
      base "/tickets"

      get :read
      index :read
      post :create
      patch :update
      delete :destroy
    end
  end
end
```

`base` sets the path prefix for all routes defined on the resource.

## Standard CRUD routes

### `get` — fetch a single record

```elixir
get :read
```

Issues a GET request to `/:id` (by default). Looks up a single record by primary key and returns a JSON:API resource object.

### `index` — list records

```elixir
index :read
```

Issues a GET request to `/` (by default). Returns a JSON:API array of resource objects. Supports filtering, sorting, pagination, and includes.

Options:
- `paginate?` (default `true`) — whether to apply pagination

### `post` — create a record

```elixir
post :create
```

Issues a POST request to `/` (by default). Accepts a JSON:API resource object in the request body and creates a record.

Options:
- `relationship_arguments` — arguments used to edit relationships inline. See the [relationships guide](/documentation/topics/relationships.md).
- `upsert?` (default `false`) — use `upsert?: true` when calling `Ash.create/2`
- `upsert_identity` — which identity to use for the upsert

### `patch` — update a record

```elixir
patch :update
```

Issues a PATCH request to `/:id` (by default). Looks up the record, then applies the update action.

Options:
- `read_action` — the read action used to look up the record before updating
- `relationship_arguments` — arguments used to edit relationships inline

### `delete` — destroy a record

```elixir
delete :destroy
```

Issues a DELETE request to `/:id` (by default). Looks up the record, then destroys it.

Options:
- `read_action` — the read action used to look up the record before destroying

### Custom paths

Any standard route can override its default path:

```elixir
patch :update_email do
  route "/update_email/:id"
end

delete :archive do
  route "/archive/:id"
end
```

## Relationship routes

These routes manage relationships following the JSON:API relationship specification. See the [relationships guide](/documentation/topics/relationships.md) for full details.

```elixir
# GET /tickets/:id/comments — returns related comment resources
related :comments, :read

# GET /tickets/:id/relationships/comments — returns resource identifiers
relationship :comments, :read

# POST /tickets/:id/relationships/comments — add to relationship
post_to_relationship :comments

# PATCH /tickets/:id/relationships/comments — replace relationship
patch_relationship :comments

# DELETE /tickets/:id/relationships/comments — remove from relationship
delete_from_relationship :comments
```

## Generic actions with `route`

The `route` helper exposes generic actions (Ash actions with `type: :action`) over any HTTP method. It is the most flexible routing option.

```elixir
route :get, "/say_hello/:name", :say_hello
route :post, "/trigger_job", :trigger_job
route :delete, "/cancel_job/:id", :cancel_job
```

There are no restrictions on the return type when using `route`. The action can return a string, map, struct, list, or nothing.

### Returning simple values

```elixir
action :say_hello, :string do
  argument :name, :string, allow_nil?: false

  run fn input, _ ->
    {:ok, "Hello, #{input.arguments.name}!"}
  end
end
```

The response body is the raw value: `"Hello, fred!"`

### Returning nothing

Actions with no return type respond with `{"success": true}` and status `201` for POST or `200` for other methods.

```elixir
action :trigger_job do
  run fn _input, _ ->
    :ok
  end
end
```

### `wrap_in_result?`

Wraps the result in a `{"result": <value>}` object:

```elixir
route :get, "/count", :count_things, wrap_in_result?: true
# Response: {"result": 42}
```

### Path parameters and query parameters

Arguments can be supplied via path parameters, query parameters, or the request body.

**Path parameters** — embed `:arg_name` segments in the route:

```elixir
route :get, "/say_hello/:name", :say_hello
# GET /say_hello/fred → name = "fred"
```

**Query parameters** — use the `query_params` option:

```elixir
route :get, "/say_hello", :say_hello, query_params: [:name]
# GET /say_hello?name=fred → name = "fred"
```

For GET requests using `route`, all action arguments are automatically accepted as query parameters even without specifying `query_params`.

**Request body** — for POST/PATCH/DELETE, remaining arguments are read from the JSON body under `data`:

```elixir
route :post, "/greet/:name", :greet
# POST /greet/fred with body {"data": {"greeting": "Hi"}}
# → name = "fred", greeting = "Hi"
```

> ### Conflicting parameters {: .warning}
>
> If the same argument appears in both the path and query string, the request returns a `400` error with an `invalid_query` error code.

## Using generic actions with standard route helpers

The standard route helpers (`get`, `index`, `post`, `patch`, `delete`) also accept generic actions, but they impose **return type constraints** so the response conforms to JSON:API format.

### Return type requirements

| Route Helper | Return Type Constraint |
|---|---|
| `route` | **None** — any return type |
| `get` | `:struct` with `instance_of: __MODULE__` |
| `index` | `{:array, :struct}` with `items: [instance_of: __MODULE__]` |
| `post` | `:struct` with `instance_of: __MODULE__` |
| `patch` | `:struct` with `instance_of: __MODULE__` + path param arguments |
| `delete` | `:struct` with `instance_of: __MODULE__` + path param arguments |

When a generic action is used with `patch` or `delete`, every path parameter (e.g. `:id`) must have a corresponding action argument — since there's no read action to look up the record, the action itself is responsible for finding it.

### Example: `get` with a generic action

```elixir
get :my_custom_get
```

```elixir
action :my_custom_get, :struct do
  constraints instance_of: __MODULE__
  argument :id, :uuid, allow_nil?: false

  run fn input, _ ->
    Ash.get(__MODULE__, input.arguments.id)
  end
end
```

The response is serialized as a standard JSON:API resource object with `type`, `id`, `attributes`, and `relationships`.

### Example: `index` with a generic action

```elixir
index :search
```

```elixir
action :search, {:array, :struct} do
  constraints items: [instance_of: __MODULE__]
  argument :query, :string, allow_nil?: false

  run fn input, _ ->
    # custom search logic
    {:ok, results}
  end
end
```

### Example: `patch` with a generic action

```elixir
patch :fake_update do
  route "/fake_update/:id"
end
```

```elixir
action :fake_update, :struct do
  constraints instance_of: __MODULE__
  argument :id, :uuid, allow_nil?: false

  run fn %{arguments: %{id: id}}, _ ->
    record = Ash.get!(__MODULE__, id)
    {:ok, %{record | name: record.name <> "_updated"}}
  end
end
```

### Example: `delete` with a generic action

```elixir
delete :fake_delete do
  route "/delete_fake/:id"
end
```

```elixir
action :fake_delete, :struct do
  constraints instance_of: __MODULE__
  argument :id, :uuid

  run fn input, _ ->
    Ash.get(__MODULE__, input.arguments.id)
  end
end
```

### When to use `route` vs standard helpers

Use the **standard helpers** when your generic action returns resource instances and you want JSON:API response formatting with `type`, `id`, `attributes`, and `relationships`.

Use **`route`** when:

- Your action returns a non-resource value (string, map, integer, etc.)
- Your action returns nothing (side-effect only)
- You want full control over the HTTP method and path
- You don't need JSON:API resource object formatting in the response

## Common route options

These options are available on all route types:

- `route` — the path for the route (can override the default)
- `action` — the action to call
- `default_fields` — a list of fields to include in the response attributes
- `primary?` (default `false`) — whether this is the default route for link generation
- `metadata` — a function `fn subject, result, request -> map end` for top-level response metadata
- `modify_conn` — a function to modify the Plug conn before responding.
- `query_params` — action arguments to accept as query parameters
- `name` — a globally unique name for this route, used in docs and OpenAPI
- `description` — a human-friendly description for generated documentation (overrides the action description)
- `derive_sort?` (default `true`) — derive a sort parameter from sortable fields
- `derive_filter?` (default `true`) — derive a filter parameter from filterable fields