---
name: fetcher-decorator-service
description: >
  Declare typed API service classes with `@ahoo-wang/fetcher-decorator`: `@api`, `@get`/`@post`/`@put`/`@patch`/`@del`, `@path`/`@query`/`@header`/`@body`, per-endpoint result extractors and `ExecuteLifeCycle` hooks. Use when endpoints are stable and should read as a class. For client setup or interceptors use fetcher-integration; for React state use fetcher-react-hooks.
---

# fetcher-decorator-service

## Decisions

- **Decorator service vs direct calls**: use a class when endpoints are stable and discoverable; call the fetcher directly (`$fetcher-integration`) when URLs or bodies are built dynamically.
- **Which fetcher**: `@api(basePath, { fetcher: 'api' })` resolves a `NamedFetcher` by name at call time, falling back to `fetcherRegistrar.default`. An instance `apiMetadata` field overrides class options; assigning a new object takes effect on the next call (mutating it in place does not).
- **Cross-cutting behaviour** (auth, retry, recovery) stays in fetcher interceptors. `afterExecute` runs only after a successful exchange — with the default status validation a 401 throws before it.

## Gotchas a capable model gets wrong

- Legacy decorators: the app needs `experimentalDecorators: true` (not `emitDecoratorMetadata`); compiled as standard decorators, `@api` throws a TypeError saying so. `reflect-metadata` ships with the package and is imported by it; no entry-point import is needed.
- Method bodies are placeholders: `throw autoGeneratedError(...args)`. If that error is actually thrown at runtime, `@api` or the method decorator is missing.
- DELETE is `@del` (not `@delete`); there is no `@trace` — use `@endpoint(HttpMethod.TRACE, path)`.
- Decorated methods default to `JsonResultExtractor` and `EndpointReturnType.RESULT`, unlike core `fetcher.get()` which returns a `Response`. Use `{ returnType: EndpointReturnType.EXCHANGE }` or a `resultExtractor` per endpoint to change it.
- Parameter names are read from the function source when omitted, which minifiers break: always pass names (`@path('id')`). `null`/`undefined` arguments are skipped. A plain object passed to `@query`/`@path`/`@header` is spread into keys; an array, `Date` or other value is bound to the parameter name (`@query('ids') ids: number[]` → `ids=1&ids=2`; an array header is comma-joined).
- An `AbortSignal` or `AbortController` argument is detected automatically; it applies together with the fetcher timeout (whichever fires first aborts).
- A subclass inherits endpoints but uses its own `@api` base path — inherited paths must still resolve there. Overriding an inherited endpoint without an endpoint decorator keeps the override's own code.
- A method called without its instance (`const { getUser } = service`) throws a TypeError; bind it or call it on the service.

## Minimal example

```ts
import { NamedFetcher } from '@ahoo-wang/fetcher';
import {
  api,
  autoGeneratedError,
  body,
  get,
  path,
  post,
  query,
} from '@ahoo-wang/fetcher-decorator';

new NamedFetcher('api', { baseURL: 'https://api.example.com' });

@api('/users', { fetcher: 'api' })
export class UserService {
  @get('/{id}')
  getUser(@path('id') id: string): Promise<User> {
    throw autoGeneratedError(id);
  }
  @get('')
  list(@query('page') page?: number): Promise<User[]> {
    throw autoGeneratedError(page);
  }
  @post('')
  create(@body() user: Omit<User, 'id'>): Promise<User> {
    throw autoGeneratedError(user);
  }
}
```

## References

- `references/api.md`: decorator signatures, parameter binding rules, execution flow, lifecycle hooks, inheritance and CRUD examples. Load it for anything beyond the example above.

## Related Skills

- $fetcher-integration: the `Fetcher`/`NamedFetcher` and interceptors underneath.
- $fetcher-llm-streaming: endpoints that return SSE streams.
- $fetcher-react-hooks: `createExecuteApiHooks` / `createQueryApiHooks` turn a service into hooks.
