I built two Quickbase Code Pages that read the same small People table and render the same HTML table. One used API_DoQuery and structured XML; the other used a REST records query and JSON. Holding the requested fields and sort order steady made it easier to see what actually changed: the request path, authorization, response representation, and the code that reads a field value.
Define the same operation first
The operation is deliberately narrow: request Record ID#, Name, Age, and Favorite Color from one People table, sort by Name ascending, and display the returned records. These examples do not apply a record filter. They are small-dataset examples, not complete pagination clients.
| Quickbase field | Field ID | Purpose |
|---|---|---|
| Record ID# | 3 | Record identity |
| Name | 6 | Display and ascending sort |
| Age | 7 | Numeric value |
| Favorite Color | 8 | Text and color swatch |
Compare the requests
const response = await fetch(`/db/${TABLE_DBID}`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/xml",
"QUICKBASE-ACTION": "API_DoQuery",
},
body: `<qdbapi>
<apptoken>${APP_TOKEN}</apptoken>
<fmt>structured</fmt>
<clist>3.6.7.8</clist>
<slist>6</slist>
<options>sortorder-A</options>
</qdbapi>`,
});The XML request targets the table in the URL. QUICKBASE-ACTION names the operation. The period-delimited clist chooses fields; slist and sortorder-A choose the ordering. The structured format is important because the field reader below looks for numeric IDs in f elements.
const query = {
from: TABLE_DBID,
select: [3, 6, 7, 8],
sortBy: [{ fieldId: 6, order: "ASC" }],
};
const response = await fetch("https://api.quickbase.com/v1/records/query", {
method: "POST",
headers: {
"QB-Realm-Hostname": window.location.hostname,
Authorization: `QB-TEMP-TOKEN ${temporaryToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(query),
});In this REST request, the endpoint identifies the operation and from identifies the table inside the JSON body. select is an array of Field IDs, and sortBy pairs a Field ID with a direction. The realm header identifies the Quickbase realm; the authorization header carries the temporary credential. This excerpt assumes temporaryToken has already been acquired.
| Choice | XML | REST |
|---|---|---|
| Table | /db/{TABLE_DBID} | from: TABLE_DBID |
| Operation | QUICKBASE-ACTION: API_DoQuery | POST /v1/records/query |
| Fields | clist: 3.6.7.8 | select: [3, 6, 7, 8] |
| Sort | slist: 6; sortorder-A | sortBy: fieldId 6, ASC |
| Parse | response.text() then DOMParser | response.json() |
| Field value | f element with id="6" | record["6"].value |
Separate application tokens from user authorization
In the XML Code Page, credentials: "include" allows the existing browser session to participate in the request. The application token is an additional app-level requirement when enabled; it does not identify a user or grant that user new permissions. Its presence should not be confused with a user token.
async function getTemporaryToken() {
const response = await fetch(
`https://api.quickbase.com/v1/auth/temporary/${TABLE_DBID}`,
{
method: "GET",
credentials: "include",
headers: {
"QB-Realm-Hostname": window.location.hostname,
"QB-App-Token": APP_TOKEN,
},
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
}
if (!data.temporaryAuthorization) {
throw new Error("Quickbase did not return temporaryAuthorization.");
}
return data.temporaryAuthorization;
}Both examples ultimately depend on the signed-in user’s access. The REST example makes the session-to-token transition visible as a separate request. A successful temporary-token request does not guarantee that every later operation is permitted. Keep temporary tokens out of finished debug output, and treat expiration and table scope as part of the request design.
Read one field from each representation
<record>
<f id="3">1</f>
<f id="6">Alice</f>
<f id="7">30</f>
<f id="8">Blue</f>
</record>{
"3": { "value": 1 },
"6": { "value": "Alice" },
"7": { "value": 30 },
"8": { "value": "Blue" }
}function getFieldValue(record, fieldId) {
const field = record.querySelector(`f[id="${fieldId}"]`);
return field ? field.textContent.trim() : "";
}function getFieldValue(record, fieldId) {
return record[String(fieldId)]?.value ?? "";
}The XML reader navigates a parsed document and returns trimmed text. The REST reader accesses an object property and its value. Nullish coalescing preserves values such as zero and false while falling back for null or undefined. This does not mean every Quickbase field becomes a simple string or number; other field types need their own handling. Here, the display code writes cell values through textContent, so returned text is displayed rather than interpreted as HTML.
Handle success, errors, and an empty result
The XML implementation checks HTTP status, XML parsing, and Quickbase’s errcode before rendering record elements. A transport response and a successful API operation are separate checks. The REST implementation checks HTTP status after parsing JSON and validates that the successful response contains a records array.
const data = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
}
if (!Array.isArray(data.data)) {
throw new Error("Quickbase did not return a records array.");
}
renderRecords(data.data); // [] clears the table and renders no rows.During review, I found that an exploratory first-record debug read assumed data.data[0] existed. An empty array would make that read fail before rendering. The downloadable version removes those inspection statements. It remains intentionally small: it does not implement token renewal, retries, or traversal of every response page. For larger datasets, inspect REST pagination metadata rather than assuming data.data contains the entire matching set.
Interpret the timing experiment honestly
Instructional simulation · api request
What the overall timer includes
These are request paths from the two implementations, not recorded timings or live API calls.
XML Code Page
- Network operation
- POST table query using the existing session
- Local work
- Read text, parse XML, extract fields, render rows
REST Code Page
- First network operation
- GET temporary authorization
- Second network operation
- POST records query with temporary token
- Local work
- Parse JSON, read values, render rows
Controlled investigation
Repeat the comparison with separate measurements
Prediction: Acquiring temporary authorization will contribute measurable time to this REST implementation.
- Use the same table, selected fields, sort order, user, and stable small dataset for both pages. Confirm record identities and field values, not just row counts.
- Measure temporary authorization, query plus response consumption, and rendering separately. Disable exploratory logging on both sides.
- Repeat multiple runs and alternate the page order. Keep browser, network, and dataset conditions as consistent as practical.
- Record whether each REST run obtains a new token or reuses a still-valid table-scoped token. Compare like request paths separately.
- Include an empty-result case. For larger tables, implement pagination before comparing complete result sets.
Interpretation: Separate measurements help explain this implementation’s cost. They still do not establish universal performance rankings across all Quickbase workloads.
Choose around the operation you need
For this operation, both implementations can supply the same displayed information. XML makes the structured-document parsing explicit; REST works directly with JavaScript objects but introduces temporary authorization in this Code Page design. Before choosing for a real project, verify support for the required operation, its authorization context, response shape, pagination, and error handling. This comparison does not establish that REST replaces every XML operation or that one API is universally preferable.