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

# Worker Code

> Your API is a plain Worker at the edge: write its code, bind storage and queues, read its errors and logs.

On the edge gateway every API runs as its own **Worker**: a standard `fetch` handler in JavaScript, executed close to the consumer. The gateway authenticates and meters consumers, then calls your Worker; your code does whatever the API does — forward to your servers, compute an answer, read a database, call several upstreams. There is no SDK to import; the same code runs unchanged in the standard Workers tooling (`wrangler dev`).

<Info>
  The **Worker** tab is available for APIs served by the edge gateway. Each API is either in **template** mode, where the Worker is generated from the [template](/studio/api-targeting), or in **code** mode, where you own the files. APIs moved from the classic gateway with endpoint-specific targets start in code mode with generated code that reproduces their classic behaviour.
</Info>

## Template or code

* **Template** (default): the Worker is generated from the template. Saving creates a preview on its own URL; **Deploy to production** in the bar on the Worker tab serves it to your consumers. **View generated code** shows exactly what runs.
* **Code**: **Edit code** stores the generated files as yours. From then on nothing is regenerated. **Deploy preview** puts your files on their own URL; **Deploy to production** (in the editor, the bar or **Promote** on the Deployments tab) serves them to your consumers. **Back to template** deletes them and generates the Worker again, as a preview too.
* **From the CLI or GitHub**: deploying files with `jojapi deploy` (or [from GitHub](/studio/github-actions)) to an API in template mode switches it to code mode. The files become a preview deployment; production keeps serving the template's deployment until you promote them (`jojapi deploy --prod` does both at once). **Back to template** restores the template.

Every save is a [deployment](/studio/deployments): an immutable snapshot with its own URL, which you can promote, roll back to or share as a pinned version.

## Files

```
index.mjs            required: export default { fetch }
endpoints/*.mjs      a handler per endpoint, if you like that layout
lib/**/*.mjs         your modules, imported with relative paths
*.json, *.txt        JSON and text modules
```

Limits: 64 files, 1 MiB per file, 6 MiB per API, folders up to 3 deep, lower-case paths. Relative imports must point at files of the API; npm packages are bundled on your side before upload (the CLI does this for you). `node:*` and `cloudflare:*` modules are available (`nodejs_compat`). Outgoing traffic is HTTP and HTTPS through `fetch()`; raw TCP sockets (`connect()` from `cloudflare:sockets`, and database drivers built on them) are not available, so reach a database through an HTTP API.

```js theme={null}
// index.mjs
import { search } from "./endpoints/search.mjs";

export default {
  async fetch(request, env, ctx) {
    switch (request.headers.get("x-jojapi-endpoint")) {
      case "GET /search":
        return search(request, env);
      default:
        return new Response(JSON.stringify({ message: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });
    }
  },
};
```

Exceptions in your code answer the consumer with a gateway `500 Server Error` and are recorded under [Runtime errors](#runtime-errors).

## The contract with the gateway

The gateway speaks to your Worker with HTTP headers only. Anything a consumer sends under `x-jojapi-*` is dropped before your Worker sees the request, so these values are trustworthy.

### Request headers your Worker receives

| Header | Value |
| - | - |
| `x-jojapi-request-id` | the request id, also the key of the entry in the request log |
| `x-jojapi-endpoint` | the matched documented endpoint, e.g. `GET /users/{id}` |
| `x-jojapi-params` | JSON object of the path parameters, e.g. `{"id":"42"}` |
| `x-jojapi-consumer` | the consumer's account id |
| `x-jojapi-user-id`, `x-jojapi-user-nick`, `x-jojapi-user-plan`, `x-jojapi-endpoint-credits` | the classic context headers (legacy user id, username, plan slug, the endpoint's fixed credits) many origins already read |
| `x-jojapi-plan-type` | `periodic` or `payasyougo` |
| `cf-connecting-ip`, `cf-ipcountry` | the consumer's IP address and country code, set by the edge |

The consumer's own headers (minus hop-by-hop ones) arrive as they were sent; the API key never does. The body streams through unread.

### Response headers your Worker may set

| Header | Meaning |
| - | - |
| `x-jojapi-usage: {"credits": 3, "tokens": 120}` | units consumed per billable object slug |
| `x-jojapi-<slug>-used: 3` | the same for one object; `x-jojapi-credits-used` is the classic form |
| `x-jojapi-error: <kind>: <message>` | a runtime error to record (e.g. `upstream: timeout for api.example.com`), never shown to consumers |
| `x-jojapi-gateway-response: true` | mark the answer as a gateway-style error: not billed |

Every `x-jojapi-*` header is stripped before the consumer sees the response; the gateway adds the usage and quota headers itself.

### Billing

Metered billable objects read their units from the **usage sources** configured in [Billable objects](/studio/billable-objects#usage-sources-on-the-edge-gateway) (a response header, a header key or a JSON field, each with a default). Where no source is configured, the `x-jojapi-usage` header applies. Platform rules stay the same: a 5xx bills nothing, a fixed amount is a ceiling, values are non-negative integers, and streaming responses (`text/event-stream`) report usage in-band as described in [Adjusting usage](/studio/adjusting-credit-usage).

## Variables

Named values bound to the Worker as `env.NAME`: upstream API keys, tokens, a proxy URL. **Secret** variables are encrypted at rest and never shown again; **plain** ones are visible. Names use capital letters, digits and underscores.

## Storage and queues

Managed storage and queues created for your API alone and bound to its Worker:

| Resource | In code |
| - | - |
| Key-value store | `await env.CACHE.get("key")`, `put`, `list` — global, eventually consistent |
| SQL database | `await env.DB.prepare("SELECT …").bind(x).all()` — SQLite |
| Object storage | `await env.FILES.get(key)`, `put(key, body)` — S3-style objects |
| Queue | `await env.JOBS.send({ … })` |
| Stateful object | `env.COUNTER.idFromName(name)`; your code exports the class: `export class Counter extends DurableObject { … }` — single-threaded objects with their own storage |

They are added and removed on the Worker tab under **Bindings**, or from the CLI with `jojapi resources add <kind> <BINDING>` (`kv`, `d1`, `r2`, `queue`, or `do --class Counter`) and `jojapi resources remove <BINDING>`, as previews like every other save. A removed key-value store, database or queue is deleted with its data once no active deployment uses it any more; object storage must be empty by then (see [Removing storage](/studio/deployments#removing-storage)). A binding name is unique across variables and storage.

A resource can also be used by other APIs — your own, or another account's after it accepts — without copying it: see [Shared resources](/studio/shared-resources).

## Console logging and limits

* **Console logging** (Worker tab → Errors & logs): captures what your Worker writes with `console.*` and the stack of uncaught exceptions. It is separate from the request log, which records every request anyway. It stays on until you switch it off, and each request then costs a little more to process: the cost appears under [Compute usage](#compute-usage) as **Console logging** (per invocation) and **Console log lines** (per stored line). The last 500 lines are kept.
* **Limits**: a request may use up to 30 seconds of CPU time and 50 outgoing requests, with an upstream call of at most 90 seconds. The platform raises these per API when an API needs more; write to support.

## Compute usage

The Worker tab (**Usage**) shows what the Worker and its storage used at the edge over the last days — requests, CPU time, key-value, SQL, object storage, stateful object and queue operations, storage, console logging — next to the included allowance and a list-price estimate. It is informational: nothing is charged for compute today, and any future charge would be announced in advance.

## Runtime errors

Whatever goes wrong inside or behind your Worker forms one stream per API, grouped by kind and message: unreachable origins and timeouts (the generated code reports them), exceptions and bad outcomes (CPU or memory limits), 5xx answers, and everything your code reports through `x-jojapi-error`. Each issue shows how often it happened, when it was first and last seen, and its recent occurrences with request ids that link back to the request log. Platform-side refusals (invalid key, quota, rate limit) are not runtime errors; they stay in the request log.

## Proxies and the egress relay

IP-address hosts and non-standard ports are reached automatically through the platform's egress relay. To send a request through your own HTTP, HTTPS or SOCKS5 proxy, set the header `x-jojapi-relay-proxy: <proxy URL>` on your `fetch()`; keep the URL in a secret variable. The template's **Proxy** field does exactly this.

The relay forwards your method, headers and body as they are: it sets `Host`, drops hop-by-hop headers and the platform's own, and adds nothing. Put proxy credentials in the URL (`http://user:pass@host:port`), not in a header. Redirects are not followed (a 3xx comes back to your Worker), and a request gives up after 90 seconds.

### Browser impersonation

Some sites answer differently depending on what the client's TLS handshake and HTTP/2 connection look like. Set `x-jojapi-relay-impersonate: <profile>` on a `fetch()` and the relay makes that request with a real browser's identity: its TLS handshake, HTTP/2 settings and default headers. It works with or without a proxy.

```js theme={null}
const res = await fetch("https://www.example.com/", {
  headers: {
    "x-jojapi-relay-impersonate": "chrome",
    "x-jojapi-relay-proxy": env.PROXY_URL, // optional
  },
});
```

* **Profiles**: `chrome`, `chrome_android`, `edge`, `firefox`, `safari` and `safari_ios` follow the newest version the platform has, or pin one such as `chrome146`, `firefox144` or `safari184`. An unknown profile answers `400`.
* **Headers**: yours replace the profile's same-named ones in place, so set your own `user-agent`, cookies or `accept-language` when you need them. The profile keeps `accept-encoding`, and the body reaches your Worker already decoded.
* The response carries `x-jojapi-relay-impersonate` with the exact profile used.
* Without the header, requests keep the default client (HTTP/1.1).
* Failures of your own proxy are named in the relay's `502` answer, for example `Proxy Tunnel Refused (407)`.

## Local development

Your Worker runs under `wrangler dev` (the standard Workers development server) like any other. Send the contract headers yourself while testing:

```bash theme={null}
curl http://127.0.0.1:8787/users/42 \
  -H 'x-jojapi-endpoint: GET /users/{id}' \
  -H 'x-jojapi-params: {"id":"42"}' \
  -H 'x-jojapi-user-nick: alice' -H 'x-jojapi-plan-type: periodic'
```

Variables become a `.dev.vars` file, resources their local `wrangler` equivalents. The [`jojapi` CLI](/studio/cli) (`jojapi pull`, `jojapi deploy`, `jojapi dev`) does this for you and bundles npm dependencies before upload. `jojapi deploy` creates a preview deployment and prints its URL; `jojapi deploy --prod` (or `jojapi promote <id>`) serves it in production.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.