# JavaScript over HTTP

> Call Refabric from Node.js with the built-in fetch: submit a task, wait for it, read its files, upload and page through lists.

Node.js 18 or later has `fetch`, `FormData` and `Blob` built in, so this client needs no package. Run it
on your server; keep the key out of the browser
([Proxy setup](https://docs.refabric.com/task-apis/calling-tasks/proxy-setup)).

## The client

```javascript refabric.mjs
const API = "https://api.refabric.com/v1";
const KEY = process.env.REFABRIC_API_KEY;

export class RefabricError extends Error {
  constructor(status, error) {
    super(`${error.code}: ${error.message}`);
    Object.assign(this, { status, ...error }); // code, type, field, retryable, request_id, …
  }
}

export async function call(method, path, { body, headers = {} } = {}) {
  const isForm = body instanceof FormData;
  const r = await fetch(path.startsWith("https://") ? path : `${API}${path}`, {
    method,
    headers: {
      Authorization: `Key ${KEY}`,
      ...(body && !isForm ? { "Content-Type": "application/json" } : {}),
      ...headers,
    },
    body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
  });
  const data = await r.json().catch(() => ({}));
  // An answer without our error object (a proxy's page, say): retry it like a network error.
  if (!r.ok) throw new RefabricError(r.status, data.error ?? { code: `http_${r.status}`, message: r.statusText, retryable: [429, 500, 502, 503, 504].includes(r.status) });
  return { status: r.status, data, headers: r.headers };
}

const sleep = (ms) => new Promise((done) => setTimeout(done, ms));

/** Submit a task and return its result once the job has ended. */
export async function run(task, input, { idempotencyKey = crypto.randomUUID(), pollMs = 5000 } = {}) {
  const { data: handle } = await call("POST", `/tasks/${task}`, { body: input, headers: { "Idempotency-Key": idempotencyKey } });
  for (;;) {
    const { data: job } = await call("GET", handle.status_url);
    if (job.lifecycle === "terminal") {
      if (job.outcome !== "succeeded") throw new RefabricError(200, job.error);
      return (await call("GET", handle.result_url)).data;
    }
    await sleep(pollMs);
  }
}

/** Every item of a list, following `next_cursor` while `has_more` is true. */
export async function* all(path) {
  let cursor;
  do {
    const sep = path.includes("?") ? "&" : "?";
    const { data } = await call("GET", cursor ? `${path}${sep}cursor=${encodeURIComponent(cursor)}` : path);
    yield* data.items;
    cursor = data.has_more ? data.next_cursor : undefined;
  } while (cursor);
}
```

## Run a task

```javascript
import { run } from "./refabric.mjs";

const result = await run("image.change_background", {
  image: "https://example.com/look.jpg",
  background: { type: "prompt", prompt: "a sunlit stone terrace" },
});
for (const file of result.files) console.log(file.file, file.url);
```

`result.files` is the first page of the job's files; when `result.has_more` is `true`, read the rest
with `?cursor=` ([Asynchronous jobs](https://docs.refabric.com/task-apis/calling-tasks/asynchronous-jobs#result)).

## Estimate first

```javascript
import { call } from "./refabric.mjs";

const { data: estimate } = await call("POST", "/tasks/image.generate/estimate", {
  body: { prompt: "a navy linen shirt dress", image_count: 4 },
});
console.log(estimate); // the most this request can cost, and its credit type
```

## Upload a file

```javascript
import { readFile } from "node:fs/promises";
import { call } from "./refabric.mjs";

const form = new FormData();
form.append("file", new Blob([await readFile("shirt.jpg")]), "shirt.jpg");
form.append("name", "Linen shirt");
const { data: upload } = await call("POST", "/files", { body: form });
console.log(upload.file); // file:… — pass it to any media field
```

The accepted file types and the size cap are `limits.upload` of `GET /v1/meta`
([Upload](https://docs.refabric.com/task-apis/files-and-media#upload)).

## Page through a list

```javascript
import { all } from "./refabric.mjs";

for await (const job of all("/jobs?lifecycle=terminal&limit=50")) console.log(job.job_id, job.outcome);
```

## Handle errors

```javascript
import { run, RefabricError } from "./refabric.mjs";

try {
  await run("image.glam", { image: "art:x1", prompt: "make the lipstick red" });
} catch (e) {
  if (!(e instanceof RefabricError)) throw e;
  if (e.retryable) {
    // retry later with backoff — and, for a submit, the same Idempotency-Key
  } else {
    console.error(e.code, e.field, e.request_id); // fix the input named by `field`
  }
}
```

Branch on `code`, never on `message` ([Task errors](https://docs.refabric.com/task-apis/errors/task-errors)). On `429`, wait
`Retry-After` seconds before the next call ([Limits](https://docs.refabric.com/task-apis/limits#rate-limits)).

## Related

::::cards
:::card{title="Client setup" href="/task-apis/calling-tasks/client-setup"}
What any HTTP client must do itself.
:::
:::card{title="Platform API" href="/api-reference/platform"}
Every operation this client can call.
:::
::::
