> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apivalk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add sorting to a list endpoint

> Declare the sortable fields on the route, let the framework validate `order_by`, and read the resolved SortBag in your controller.

Sorting is route-level. You declare which fields the endpoint will sort by (and their default direction); clients send `?order_by=...` to override it; the framework parses, validates, and populates `$request->sorting()` for you.

## 1. Declare sortable fields

```php theme={null}
use apivalk\apivalk\Router\Route\Route;
use apivalk\apivalk\Router\Route\Sort\Sort;

public static function getRoute(): Route
{
    return Route::get('/api/v1/animals')
        ->sorting([
            Sort::asc('name'),
            Sort::desc('created_at'),
        ]);
}
```

`Sort::asc('name')` means: "sortable on `name`; if the client doesn't override, default direction is ascending". `Sort::desc(...)` is the reverse.

## 2. Clients pass `order_by`

The format is a comma-separated list of field names, each optionally prefixed with `+` (ascending, default) or `-` (descending):

```
GET /api/v1/animals                              # default sorting
GET /api/v1/animals?order_by=name                # name ASC
GET /api/v1/animals?order_by=-created_at         # created_at DESC
GET /api/v1/animals?order_by=name,-created_at    # name ASC, created_at DESC
```

`RequestValidationMiddleware` rejects anything not declared — `?order_by=hacked_field` → `422`. Routes without `sorting([...])` skip this check entirely (nothing to validate against).

## 3. Read the resolved sorts

`$request->sorting()` returns a `SortBag`. It implements `IteratorAggregate`, so iterate directly:

```php theme={null}
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
    $qb = $this->animals->query();

    foreach ($request->sorting() as $field => $sort) {
        $qb->orderBy($field, $sort->isAsc() ? 'ASC' : 'DESC');
    }

    // ...
}
```

The bag is **always populated** — if the client omits `order_by`, it contains the defaults from `Sort::asc(...)` / `Sort::desc(...)` in the order you declared them.

Iteration order matches user intent: any sorts the client submitted via `?order_by=` come first (in submission order), followed by the route's defaults for fields the client didn't specify. So `foreach` produces the right `ORDER BY` sequence — primary user choice first, defaults as tiebreakers — without you having to merge anything.

## 4. Tell user-requested sorts from defaults

Each `Sort` knows whether it came from `?order_by=` or from the route declaration. Branch on `isRequested()` when you need to:

```php theme={null}
if ($request->sorting()->created_at->isRequested()) {
    // The client explicitly asked to sort by created_at.
}
```

To get only the user's submitted sorts (without route defaults), use `getRequested()`:

```php theme={null}
foreach ($request->sorting()->getRequested() as $sort) {
    // Empty array if the client did not send order_by at all.
}
```

<Note>
  `has($field)` only tells you whether the field is in the bag at all — for declared fields, that's always `true` because defaults seed the bag. Use `isRequested()` to ask "did the user ask for this?".
</Note>

## Inside a resource

Resources expose `availableSortings()`:

```php theme={null}
public function availableSortings(): array
{
    return [Sort::asc('name'), Sort::desc('created_at')];
}
```

`AbstractListResourceController` reads this and calls `$route->sorting([...])` for you. The `DocBlock` generator also emits typed `@method` annotations on `*ListRequest` so `$request->sorting()->name` autocompletes. See [generate OpenAPI + docblocks](/how-to/generate-openapi).

## OpenAPI side effects

One query parameter — `order_by` — is added to the operation with:

* `type: string`
* Description listing supported fields and the `+` / `-` syntax.
* Example matching your declared defaults.

## Reference

Full behaviour and edge cases live at [HTTP / Sorting](/http/sorting).
