# Talking to the server from the frontend: server.request

> Audience: anyone writing CBX frontend or admin JavaScript. TL;DR: server.makeRequest handed you a jqXHR and left you to figure out what happened. server.requ…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/frontend_requests. Last updated 2026-08-01.

---
**Audience:** anyone writing CBX frontend or admin JavaScript.
**TL;DR:** `server.makeRequest()` handed you a jqXHR and left you to figure out what happened.
`server.request()` hands you an *answer*. One `await`, one `if`, done.

---

## The thirty-second version

Before:

```js
server.makeRequest('reviews', 'storeReview', data)
    .done(function(response) {
        wrapper.find('.feedback-message').text(response.feedback);
        wrapper.find('.review-form').slideUp();
    });
```

After:

```js
const response = await server.request('reviews', 'storeReview', data);

if (!response.ok) {
    wrapper.find('.feedback-message').text(response.errors.join(' '));
    return;
}

wrapper.find('.feedback-message').text(response.messages.join(' '));
wrapper.find('.review-form').slideUp();
```

Four lines longer, and it is the difference between a review form that lies to the customer and one
that doesn't. Read on for why — the first example is real code, and it has a real bug.

---

## Why the old way quietly broke things

`makeRequest` returns jQuery's `jqXHR`. That sounds convenient, and it is, right up until you ask a
simple question: **how do I know it worked?**

There is no single answer, because CBX endpoints could fail in three completely different ways:

1. **The request never arrived.** Network down, tab closing. → `.fail()`
2. **The server refused.** Not logged in, validation failed, record in use. → **`.done()`**, because
   the legacy `ConfigboxJsonResponse` answered HTTP 200 with `{"success": false, …}`
3. **It worked.** → `.done()`

Look at (2) and (3). *Both land in `.done()`.* The only thing separating a saved review from a
rejected one was a `success` field that you had to remember to check.

The example above doesn't check it. So when the server refuses to store a review, that code writes
whatever happens to be in `response.feedback` into the page and **slides the form away as if the
review had been saved**. The customer walks off believing they posted a review that does not exist.

Nobody was careless here. The pattern simply makes the wrong thing shorter to write than the right
thing, and short wins on a Friday afternoon. That is the actual problem being fixed: not ugliness,
but a default that costs you correctness.

It gets sharper. As endpoints migrate to real HTTP status codes, a refusal becomes a genuine 4xx —
which routes to `.fail()`. Old code with only a `.done()` handler now does **nothing at all**: no
message, no error, a button that spins forever. Code that was *subtly* wrong becomes *visibly*
broken. Migrating the caller is not optional cleanup; it is what keeps the feature working.

---

## What the server says now

Two shapes, and the **HTTP status tells you which** — you never have to guess.

**Success — 200, or 201 when something was created:**

```json
{
  "data": { "cartPositionId": 42, "finished": true },
  "meta": { "feedback": "Saved." }
}
```

`data` is the payload. `meta` is cross-cutting stuff — the human-facing `feedback` line lives there.
Note what is *absent*: there is no `success` field. The status already said so, and a second source
of truth is just an opportunity for the two to disagree.

**Failure — 4xx or 5xx, as [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457):**

```json
{
  "type": "https://www.configbox.at/api/errors/record-in-use",
  "title": "Conflict",
  "status": 409,
  "code": "RECORD_IN_USE",
  "detail": "Could not delete the page, because it contains questions.",
  "errors": [
    { "code": "RECORD_IN_USE", "message": "Could not delete the page, because it contains questions." }
  ],
  "validationIssues": []
}
```

The field that matters most for code is **`code`** — a stable, machine-readable identifier
(`RECORD_IN_USE`, `CONFIGURATION_INCOMPLETE`, `UNKNOWN_ANSWER`). Branch on that, never on `detail`,
which is prose and is translated.

`validationIssues` carries field-level problems, each `{field, code, message}`, so a form can put the
message next to the input it belongs to instead of dumping everything at the top.

> The server side of this is `ConfigboxApiResponse` — `ok()`, `created()`, `notFound()`, `conflict()`,
> `validationFailed()` and friends. See `technical/com_configbox_api_contract.md`.

---

## What you get back: `CbResponse`

You don't parse any of the above. `server.request()` resolves with one normalised object, whichever
shape came back:

| Field | Type | What it is |
|---|---|---|
| `ok` | boolean | Did it work? Exactly "HTTP status below 400", nothing else. |
| `status` | number | The HTTP status. |
| `data` | object | Payload on success. `{}` otherwise — **never null**, so no guard needed. |
| `meta` | object | Cross-cutting extras on success (e.g. `meta.feedback`). |
| `errors` | string[] | Human-readable failure messages, ready to display. `[]` when `ok`. |
| `messages` | string[] | Human-readable success messages, ready to display. |
| `validationIssues` | array | `{field, code, message}` per field problem. `[]` when `ok`. |
| `code` | string | Stable machine error code. `''` when `ok`. |
| `problem` | object\|null | The raw problem body, if you need something exotic. |
| `raw` | string | The unparsed body, for diagnostics. |

Two properties of this design are worth naming, because they remove entire categories of bug:

**Every collection is always a collection.** `errors`, `messages` and `validationIssues` are arrays
even on success. You can `.join()`, `.forEach()` and `.length` them without a null check, every time.

**The promise resolves for every HTTP status.** A 409 is an *answer*, not a lost request. It rejects
only when the request genuinely never completed — network down, request aborted. So the shape of
your code is:

```js
let response;

try {
    response = await server.request('admincurrencies', 'delete', { ids: '4,7' });
}
catch (e) {
    // The request never left the building. Genuinely exceptional.
    return;
}

if (!response.ok) { /* the server answered, and said no */ }
```

…and in practice you rarely write the `try` at all, because a dead network is not usually something
a click handler can do anything about.

---

## Reacting to a response

### The everyday shape

This is the great majority of call sites. Real code, from `adminCurrencies.js`:

```js
const response = await server.request('admincurrencies', task, {
    ids: kenedo.getCheckedListItemIds(list).join(',')
});

if (!response.ok) {
    kenedo.showResponseMessages(list, response.errors, response.messages);
    return;
}

kenedo.reloadListWithMessages(list, response);
```

Guard clause, early return, happy path unindented at the bottom. No nesting, no `.done`/`.fail`
split, no reading a `success` field.

### Reading the payload

```js
const response = await server.request('configuratorpage', 'getConfiguration', {
    cartPositionId: positionId
});

if (!response.ok) {
    return;
}

// `data` is always an object, so this needs no guard.
const selections = response.data.selections || [];
```

### Field-level validation

```js
if (!response.ok) {

    response.validationIssues.forEach(function(issue) {
        form.find('[name="' + issue.field + '"]')
            .closest('.form-group')
            .addClass('has-error')
            .find('.validation-message-target')
            .text(issue.message);
    });

    // Anything not tied to a field still belongs somewhere visible.
    if (response.validationIssues.length === 0) {
        showErrors(response.errors);
    }

    return;
}
```

### Branching on *why* it failed

This is what stable `code`s are for:

```js
if (!response.ok) {

    if (response.code === 'CONFIGURATION_INCOMPLETE') {
        // Ride-along detail: which questions are still open.
        jumpToFirstOpenQuestion(response.problem.errors);
        return;
    }

    if (response.code === 'AUTHENTICATION_REQUIRED') {
        showLoginPrompt();
        return;
    }

    showErrors(response.errors);
    return;
}
```

Never branch on `detail` or on `errors[0]`. Those are prose, they are translated, and they will be
reworded by someone who has no idea your `if` depends on the exact wording.

### Several calls in a row

The part that used to be genuinely unpleasant — sequencing — is now boring:

```js
const started = await server.request('configuratorpage', 'startConfiguration', { prod_id: id });

if (!started.ok) {
    return showErrors(started.errors);
}

const positionId = started.data.cartPositionId;

const selected = await server.request('configuratorpage', 'setSelection', {
    cartPositionId: positionId,
    questionId: questionId,
    selection: answerId
});

if (!selected.ok) {
    return showErrors(selected.errors);
}
```

Try writing that with nested `.done()` callbacks and you will remember why nobody enjoyed it.

---

## Three doors, one answer

All three resolve with the same `CbResponse`. Pick by what you're sending:

| Use | When |
|---|---|
| `server.request(controller, task, data)` | The normal way. Builds the URL from controller + task. |
| `server.requestUrl(url, data)` | You were *handed* a finished URL by the server and rebuilding it client-side would mean duplicating the host's SEF routing. The quick-edit toolbar is the case. |
| `server.submitForm(formElement, extraData)` | A real Kenedo detail form, posted as `FormData` to its own action — the one that carries file inputs. |

Because the return shape is identical, the code that reacts to them is identical too. That is the
whole point of a choke point: exactly one place in the product understands what a CBX endpoint
can answer, and it is `server.normaliseResponse()`.

**The one real gap:** `request()` sends ordinary parameters, not `File` objects. If you are uploading
a file outside a Kenedo form, `makeRequest()` still has bespoke `File`/`FileList` handling that
`request()` does not. That is the single case where reaching for the old function is defensible
today.

---

## Migrating a call site

A recipe that fits most of them:

1. Make the enclosing function `async`.
2. `const response = await server.request(...)` in place of `server.makeRequest(...)`.
3. Move the `.done(...)` body down, unindented.
4. Add `if (!response.ok) { …show response.errors…; return; }` above it.
5. Replace `response.foo` with `response.data.foo` — the payload moved under `data`.
6. Replace `response.feedback` with `response.messages` (an array) or `response.meta.feedback`.
7. A `.always(...)` becomes a `finally { }` block.

Worked, on the broken review form from the top:

```js
// BEFORE
server.makeRequest('reviews', 'storeReview', data)
    .done(function(response) {
        wrapper.find('.feedback-message').text(response.feedback);
        wrapper.find('.review-form').slideUp();
    })
    .always(function() {
        button.removeClass('processing');
    });

// AFTER
try {
    const response = await server.request('reviews', 'storeReview', data);

    if (!response.ok) {
        wrapper.find('.feedback-message').text(response.errors.join(' '));
        return;                       // form stays open, so the customer can retry
    }

    wrapper.find('.feedback-message').text(response.messages.join(' '));
    wrapper.find('.review-form').slideUp();
}
finally {
    button.removeClass('processing');
}
```

The bug is gone, and it is gone *structurally* — not because this version remembers to check, but
because `ok` is the first thing you look at and `errors` is already a displayable array.

### ⚠️ One AMD trap, and it is a nasty one

RequireJS **silently discards an `async` module callback**. No error, no warning, the module simply
never initialises:

```js
// BROKEN — the callback never runs, and nothing tells you
cbrequire(['configbox/server'], async function(server) { … });
```

Keep the module callback synchronous and put `async` on the function *inside* it:

```js
cbrequire(['configbox/server'], function(server) {

    cbj(document).on('click', '.my-button', async function() {
        const response = await server.request('mycontroller', 'mytask', {});
        …
    });

});
```

This has already cost real debugging time. If a handler you just wrote appears to do absolutely
nothing, check this before anything else.

---

## Things not to do

**Don't read `success`.** On a migrated endpoint the field is gone, and `undefined` is falsy — so
`if (response.success)` turns every successful call into a failure. The status is the outcome.

**Don't add `.fail()` to `request()`.** It returns a native promise, not a jqXHR. Failures arrive as
a resolved `CbResponse` with `ok: false`.

**Don't parse the body yourself.** If you find yourself reaching into `response.raw` or
`response.problem` for something routine, that is a gap in `normaliseResponse()` — fix it there, once,
for everybody.

**Don't branch on message text.** Use `response.code`.

---

## During the migration, both shapes are alive

Not every endpoint has been migrated. `normaliseResponse()` handles both:

- **migrated** — the status is the outcome; 2xx is `{data, meta}`, 4xx/5xx is problem details
- **legacy** — always 200, with `ConfigboxJsonResponse`'s `{success, errors, messages}`

For a legacy endpoint it *does* read `success`, because on that shape it genuinely is the outcome.
Either way you get the same `CbResponse`, so **your calling code does not need to know which kind of
endpoint it is talking to** — and it will not need changing when that endpoint migrates. Adopting
`server.request()` now is how you avoid a second pass later.

---

## Where to look next

- `technical/com_configbox_api_contract.md` — the envelope, the status codes, the stable error codes
- `technical/com_configbox_runtime_api.md` — configuring a product and working the cart headlessly
- `customization/com_configbox_assets_and_amd.md` — how frontend JS is loaded and overridden

---

<!-- MAINTENANCE ------------------------------------------------------------------------------
     This document describes concrete functions. If you change any of them, update it in the
     same commit — a migration guide that describes last year's API is worse than none, because
     people trust it.

     Documented here, all in assets/javascript/server.js unless noted:

       server.request()            signature, POST-only behaviour, resolve-on-every-status rule
       server.requestUrl()         when a finished URL is used instead of controller + task
       server.submitForm()         FormData path for Kenedo detail forms
       server.normaliseResponse()  the CbResponse field list, and the legacy/migrated branch
       server.makeRequest()        the deprecated path + its File/FileList handling (the one gap)
       CbResponse typedef          the field table above is a copy of it — keep them in step

       classes/ConfigboxApiResponse.php   the success {data, meta} and RFC 9457 problem shapes

     Specifically re-check the CbResponse field table and the "Things not to do" list, since both
     encode current behaviour rather than intent.
------------------------------------------------------------------------------------------- -->
