# Typed Requests & Errors

> Call any LearnHouse API endpoint with full types, handle errors, and control timeouts, retries, rate limits, pagination, cancellation and hooks in @learnhouse/sdk.

## Any endpoint, typed

`lh.GET`, `lh.POST`, `lh.PUT`, `lh.PATCH` and `lh.DELETE` take an API path exactly as the
[API reference](https://docs.learnhouse.app/reference) lists it. Paths autocomplete, parameters and bodies are checked, and the result is typed
from the endpoint's response schema.

```ts
// Path and query parameters
const meta = await lh.GET('/api/v1/courses/{course_uuid}/meta', {
  params: { path: { course_uuid: 'course_…' }, query: { slim: true } },
})

// JSON body
const chapter = await lh.POST('/api/v1/chapters/', {
  body: { name: 'Week 2', course_id: 12, org_id: 1 },
})
```

A missing path parameter, a wrong type or an unknown path is a compile error. Calls return the response data directly,
or throw.

### Files and forms

Pass `FormData` as the body for uploads, or `bodyType: 'form'` to send a plain object as form fields:

```ts
const form = new FormData()
form.append('thumbnail', file)
await lh.PUT('/api/v1/courses/{course_uuid}/thumbnail', {
  params: { path: { course_uuid: 'course_…' } },
  body: form,
})
```

### Other response formats

`parseAs` reads the body as `'json'` (default for JSON), `'text'`, `'blob'`, `'arrayBuffer'`, `'stream'`, or gives you the
raw `'response'`.

### Endpoints outside the schema

`lh.request(method, path, options)` sends any request with the same auth, retries and errors, without types.

## Errors

Every failure throws a `LearnHouseError`:

```ts
import { LearnHouseError, isLearnHouseError } from '@learnhouse/sdk'

try {
  await lh.activities.get('activity_…')
} catch (err) {
  if (isLearnHouseError(err)) {
    console.log(err.status) // 404
    console.log(err.code)   // 'HTTP_ERROR', or the API's own code like 'RATE_LIMITED'
    console.log(err.detail) // the API's `detail`
  } else {
    throw err
  }
}
```

| Field | |
| --- | --- |
| `status` | HTTP status (`0` when there was no response) |
| `code` | `HTTP_ERROR`, `NETWORK_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `VALIDATION_ERROR`, or the code the API sent |
| `detail` | The API's `detail`: a message, an object, or a list of validation errors |
| `method`, `path`, `url` | The request that failed |
| `body`, `cause` | The raw error body, and the underlying error for network failures |
| `retryAfterMs` | How long the API asked you to wait, when it sent `Retry-After` (on `429` and `503`) |

`isLearnHouseError` also recognises errors from another copy of the SDK (two bundles on one page).

## Timeouts, retries, cancellation

```ts
const lh = createClient({
  baseUrl,
  timeoutMs: 15_000, // per attempt, body included. 0 turns it off
  retry: 3,          // or { retries, baseDelayMs, maxDelayMs }. false turns it off
})

// Per call
const controller = new AbortController()
await lh.courses.getMeta('course_…', { signal: controller.signal, timeoutMs: 5_000, retry: false })
```

The SDK retries:

- `GET` and `HEAD` requests that hit a network error, a timeout or a `5xx`.
- Any request that gets a `429 Too Many Requests`. The API refused it before doing anything, so sending it again is
safe.

Retries back off exponentially, starting at `baseDelayMs` (300 ms) and capped at `maxDelayMs` (5 s). Other requests
that change data are never retried, so nothing is applied twice.

Aborting stops the request, and any wait between retries, and throws the platform's `AbortError`, not a
`LearnHouseError`.

### Rate limits

When a `429` or `503` carries a `Retry-After` header, the SDK waits exactly that long before the next attempt, instead
of its own backoff. Both forms work: seconds (`Retry-After: 30`) and an HTTP date.

If the API asks for a longer wait than `maxDelayMs`, the SDK does not retry early, since that only earns another
`429`. It throws right away, and `err.retryAfterMs` tells you when to try again:

```ts
try {
  await lh.search.org('your-school', 'payouts')
} catch (err) {
  if (isLearnHouseError(err) && err.status === 429) {
    console.log(`Try again in ${err.retryAfterMs ?? 1000} ms`)
  } else {
    throw err
  }
}
```

Raise `maxDelayMs` if your code can afford to wait longer, for example in a background job.

## Pagination

`lh.courses.listAll()` walks every page for you. Use it with `for await`:

```ts
for await (const course of lh.courses.listAll('your-school')) {
  console.log(course.name)
}
```

It asks for 100 courses per request (the API's maximum) and stops after the first page that comes back short. It
never requests a page past the end. `break` out of the loop and no further page is fetched.

For other endpoints that take a page and a limit, use `paginate()`. Give it a function that fetches one page:

```ts
import { paginate } from '@learnhouse/sdk'

const controller = new AbortController()
const pages = paginate(
  ({ page, pageSize, signal }) => lh.courses.list('your-school', { page, limit: pageSize, signal }),
  { pageSize: 50, signal: controller.signal },
)

for await (const course of pages) {
  if (course.name === 'Payouts') break
}
```

| Option | Default | |
| --- | --- | --- |
| `pageSize` | `20` | Items per request. Keep it at or under the endpoint's maximum, or a capped page reads as the last one |
| `startPage` | `1` | First page to fetch |
| `signal` | | Stops between pages. Pass it to your own call too, as above |

Search results are paged per result type, so they have their own iterator. See
[Search](https://docs.learnhouse.app/developers/sdk/resources#search).

## Hooks

`onRequest` runs before each attempt and can change the request; `onResponse` sees every response.

```ts
const lh = createClient({
  baseUrl,
  token,
  onRequest: (ctx) => {
    ctx.init.headers.set('X-Request-Id', crypto.randomUUID())
  },
  onResponse: (ctx) => {
    metrics.timing('learnhouse.request', ctx.path, ctx.response.status)
  },
})
```

`ctx.path` is the path template (`/api/v1/courses/{course_uuid}`), which makes it a good metrics label.

## Custom fetch

Pass `fetch` to use your own implementation: a proxy-aware one in Node, Next.js's cached `fetch`, or a mock in tests.

```ts
const lh = createClient({ baseUrl, token, fetch: (url, init) => fetch(url, { ...init, next: { revalidate: 60 } }) })
```

> Types come from the API of the release the SDK was published with. If your instance is older, a few newer endpoints
> may not exist on it yet; see [Versions & releases](https://docs.learnhouse.app/developers/versioning).
