Lesson 3
Code Page Here - PeoplePage_search.htmlQuery Quickbase with Search Criteria
Instead of retrieving every record and deciding what we want afterward, tell Quickbase which records we want before it returns anything.
Why the Roadmap Changes Here
Lesson 2 proved an important point: Quickbase can return records to JavaScript, JavaScript can retain those records, and the browser can manipulate them without another API request.
Sorting was enough to prove that concept.
Rather than spend several lessons repeating client-side manipulation, Lesson 3 returns our attention to the Quickbase REST API itself.
The new question is:
Can we control which records Quickbase returns in the first place?
1. Control the Result Set Before It Returns
Our earlier REST examples could request the People table and receive a collection of records.
Lesson 3 adds criteria to that REST query.
2. Start with No Records
The page deliberately begins with an empty table.
document.addEventListener(
"DOMContentLoaded",
() => {
document
.getElementById("sortName")
.addEventListener("click", sortByName);
document
.getElementById("searchButton")
.addEventListener(
"click",
searchByFavoriteColor
);
}
);The important detail is what is not there:
loadPeople();The Code Page does not query Quickbase simply because the page opened.
Clarification: Why Was the Whole Table Appearing First?
During development, the search itself worked, but the page still returned the complete People dataset before the learner searched.
The reason was simple:
loadPeople();was still running during DOMContentLoaded.
A REST query with no search condition naturally returned the normal result set. Removing that initial call changed the page from:
to:
3. Read the Search Value
const input =
document.getElementById(
"favoriteColorSearch"
);
const color =
input.value.trim();This portion is ordinary browser JavaScript. The Quickbase-specific work begins when that value is converted into query criteria.
4. No Search Criteria Means No Query
if (!color) {
const status =
document.getElementById("status");
peopleRecords = [];
renderRecords(peopleRecords);
status.classList.remove("error");
status.textContent =
"Enter a Favorite Color before searching.";
return;
}This creates a deliberate rule for the lesson:
No criteria = no Quickbase record query.
Clarification: Why Not Let a Blank Search Return Everything?
An earlier version called loadPeople() when the search box was empty. That made an empty search behave like “show all records.”
That behavior was valid JavaScript, but it contradicted the purpose of this lesson.
We are specifically learning to request only what is needed. Therefore a missing condition should not silently become a request for the entire table.
5. Build a Quickbase Query Expression
The new Quickbase-specific line is:
const where =
`{${FIELD_IDS.favoriteColor}.EX.'${color}'}`;If:
FIELD_IDS.favoriteColor = 8and the learner enters:
Bluethe final Quickbase expression becomes:
{8.EX.'Blue'}6. Read the Query from Left to Right
| Part | Meaning |
|---|---|
8 | Quickbase Favorite Color Field ID. |
EX | Exact-match comparison. |
'Blue' | The value the learner entered. |
7. Pass the Criteria into the REST Query
Once the Quickbase expression is built:
loadPeople(where);loadPeople() now accepts the condition:
async function loadPeople(where = "") {The default value preserves a reusable function signature, but the Lesson 3 interface does not automatically call it with an empty condition.
8. Add where to the Quickbase JSON Query
const query = {
from: TABLE_DBID,
where,
select: [
FIELD_IDS.recordId,
FIELD_IDS.name,
FIELD_IDS.age,
FIELD_IDS.favoriteColor,
],
options: {
top: 200,
skip: 0,
},
sortBy: [
{
fieldId: FIELD_IDS.name,
order: "ASC",
},
],
};The major Lesson 3 addition is simply:
where,But that small JSON property fundamentally changes which records Quickbase returns.
What Quickbase Receives
Searching for Blue effectively produces a query resembling:
{
"from": "YOUR_TABLE_DBID",
"where": "{8.EX.'Blue'}",
"select": [
3,
6,
7,
8
],
"options": {
"top": 200,
"skip": 0
},
"sortBy": [
{
"fieldId": 6,
"order": "ASC"
}
]
}Quickbase evaluates the where condition and returns only records that qualify.
9. Lesson 2 and Lesson 3 Are Doing Different Work
| Concept | Lesson 2 | Lesson 3 |
|---|---|---|
| Initial page load | Query all People records | Query no records |
| User action | Sort already-loaded records | Enter search criteria |
| Where operation happens | Browser JavaScript | Quickbase |
| Additional REST request | No | Yes |
| Returned dataset | All queried records | Only matching records |
Clarification: This Is Not Client-Side Searching
Lesson 2 retained Quickbase records in peopleRecords and then manipulated that array directly.
Lesson 3 does something different.
This distinction was important enough that the interaction itself was changed to say Search Quickbase rather than merely Search.
10. Make the Experiment Obvious to the Learner
The finished Code Page contains an instructional interaction panel because searching is the central experiment in this lesson.
Try It Yourself
Search Quickbase by Favorite Color
The table starts empty. Enter a favorite color and click Search Quickbase to request only matching People records.
- You enter a favorite color.
- You click Search Quickbase.
- The Code Page sends your criteria to Quickbase.
- Quickbase finds only matching records.
- The matching records are returned and displayed.
11. The Complete Lesson 3 Flow
The Bigger Idea
Lesson 2 taught:
Quickbase gave us the data. Now JavaScript can control it.
Lesson 3 adds:
We can also control which data Quickbase gives us.
Those are two different kinds of control, and understanding that distinction gives us a much stronger foundation for more advanced Quickbase queries.
Complete Lesson 3 Source
The training source below preserves the REST architecture and Lesson 2 sorting behavior while emphasizing the Lesson 3 changes: delayed querying, Quickbase search criteria, and the REST where property.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>People Table</title>
<style>
body {
margin: 0;
padding: 32px;
background: #f4f6f8;
color: #1f2933;
font-family: Arial, Helvetica, sans-serif;
}
.page {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin: 0 0 8px;
color: #003366;
}
.description {
margin: 0 0 24px;
color: #5b6775;
}
.status {
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid #cfd8e3;
border-radius: 6px;
background: #ffffff;
}
.status.error {
border-color: #a61c1c;
color: #7f0000;
background: #fff1f1;
}
.table-container {
overflow-x: auto;
border: 1px solid #d7dee7;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid #e5e9ef;
text-align: left;
}
th {
background: #003366;
color: #ffffff;
}
tbody tr:hover {
background: #f3f7fb;
}
.color-value {
display: inline-flex;
align-items: center;
gap: 8px;
}
.color-swatch {
width: 18px;
height: 18px;
border: 1px solid #7f8c99;
border-radius: 4px;
}
.search-controls {
margin-bottom: 24px;
padding: 24px;
border: 1px solid #b8c9da;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 51, 102, 0.08);
}
.search-intro {
margin-bottom: 20px;
}
.search-eyebrow {
margin: 0 0 6px;
color: #1f5c99;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.search-intro h3 {
margin: 0 0 8px;
color: #003366;
font-size: 22px;
}
.search-intro p {
max-width: 760px;
margin: 0;
color: #52606d;
line-height: 1.6;
}
.search-action {
display: flex;
align-items: end;
gap: 12px;
margin-bottom: 20px;
}
.search-field {
flex: 1;
max-width: 420px;
}
.search-field label {
display: block;
margin-bottom: 6px;
font-weight: 700;
color: #263746;
}
.search-field input {
box-sizing: border-box;
width: 100%;
padding: 11px 12px;
border: 1px solid #9fb3c8;
border-radius: 6px;
background: #ffffff;
color: #1f2933;
font: inherit;
}
.search-field input:focus {
outline: 3px solid rgba(31, 92, 153, 0.15);
border-color: #1f5c99;
}
.search-action button {
padding: 11px 18px;
border: 1px solid #174a7c;
border-radius: 6px;
background: #1f5c99;
color: #ffffff;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.search-action button:hover {
background: #174a7c;
}
.search-steps {
padding: 16px 18px;
border-left: 4px solid #1f5c99;
background: #f3f7fb;
color: #364957;
}
.search-steps ol {
margin: 10px 0 0;
padding-left: 22px;
}
@media (max-width: 640px) {
.search-action {
align-items: stretch;
flex-direction: column;
}
.search-field {
max-width: none;
}
.search-action button {
width: 100%;
}
}
</style>
</head>
<body>
<main class="page">
<h1>People</h1>
<h2>
Lesson 3 — Search Quickbase → Return Matching Records → Render Results
</h2>
<p class="description">
Request only the People records that match the search criteria.
</p>
<div id="status" class="status">
Enter a Favorite Color and click Search.
</div>
<div class="search-controls">
<div class="search-intro">
<p class="search-eyebrow">
Try It Yourself
</p>
<h3>
Search Quickbase by Favorite Color
</h3>
<p>
The table below starts empty.
Enter a favorite color and click
<strong> Search Quickbase</strong>
to request only matching People records.
</p>
</div>
<div class="search-action">
<div class="search-field">
<label for="favoriteColorSearch">
Favorite Color
</label>
<input
type="text"
id="favoriteColorSearch"
placeholder="Example: Blue"
/>
</div>
<button
type="button"
id="searchButton"
>
Search Quickbase
</button>
</div>
<div class="search-steps">
<strong>
What happens when you search?
</strong>
<ol>
<li>You enter a favorite color.</li>
<li>You click Search Quickbase.</li>
<li>The search criteria is sent to Quickbase.</li>
<li>Quickbase finds matching records.</li>
<li>Only those records are returned.</li>
</ol>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Record ID</th>
<th>
<button
type="button"
id="sortName"
>
Name
</button>
</th>
<th>Age</th>
<th>Favorite Color</th>
</tr>
</thead>
<tbody id="peopleRows"></tbody>
</table>
</div>
</main>
<script>
/*
* ============================================================
* QUICKBASE CONNECTION INFORMATION
* ============================================================
*/
const REALM =
window.location.hostname.includes("quickbase.com")
? window.location.hostname
: "YOUR_REALM.quickbase.com";
const TABLE_DBID =
"YOUR_TABLE_DBID";
const APP_TOKEN = "";
let peopleRecords = [];
let nameSortDirection = "asc";
const FIELD_IDS = {
recordId: 3,
name: 6,
age: 7,
favoriteColor: 8,
};
function createCell(value) {
const cell =
document.createElement("td");
cell.textContent = value;
return cell;
}
function createColorCell(colorName) {
const cell =
document.createElement("td");
const wrapper =
document.createElement("span");
wrapper.className =
"color-value";
const swatch =
document.createElement("span");
swatch.className =
"color-swatch";
swatch.style.backgroundColor =
colorName || "transparent";
const label =
document.createElement("span");
label.textContent =
colorName;
wrapper.appendChild(swatch);
wrapper.appendChild(label);
cell.appendChild(wrapper);
return cell;
}
async function readQuickbaseResponse(response) {
const rawBody =
await response.text();
try {
return {
parsed: JSON.parse(rawBody),
rawBody,
};
} catch {
return {
parsed: null,
rawBody,
};
}
}
function getFieldValue(record, fieldId) {
return record[String(fieldId)]?.value ?? "";
}
function renderRecords(records) {
const tableBody =
document.getElementById("peopleRows");
tableBody.replaceChildren();
for (const record of records) {
const recordId =
getFieldValue(
record,
FIELD_IDS.recordId
);
const name =
getFieldValue(
record,
FIELD_IDS.name
);
const age =
getFieldValue(
record,
FIELD_IDS.age
);
const favoriteColor =
getFieldValue(
record,
FIELD_IDS.favoriteColor
);
const row =
document.createElement("tr");
row.appendChild(
createCell(recordId)
);
row.appendChild(
createCell(name)
);
row.appendChild(
createCell(age)
);
row.appendChild(
createColorCell(favoriteColor)
);
tableBody.appendChild(row);
}
}
async function getTemporaryToken() {
const headers = {
"QB-Realm-Hostname": REALM,
};
if (APP_TOKEN.trim()) {
headers["QB-App-Token"] =
APP_TOKEN;
}
const response = await fetch(
`https://api.quickbase.com/v1/auth/temporary/${TABLE_DBID}`,
{
method: "GET",
credentials: "include",
headers,
},
);
const {
parsed,
rawBody,
} = await readQuickbaseResponse(
response
);
if (!response.ok) {
const details =
parsed
? JSON.stringify(parsed)
: rawBody;
throw new Error(
`HTTP ${response.status}: ${details}`
);
}
if (!parsed?.temporaryAuthorization) {
throw new Error(
"Quickbase did not return temporaryAuthorization."
);
}
return parsed.temporaryAuthorization;
}
/*
* ============================================================
* LESSON 2 FUNCTION RETAINED
* ============================================================
*
* Sorting still operates on the records that Quickbase has
* already returned.
*
* This lets us directly compare client-side manipulation with
* Lesson 3's server-side query.
*/
function sortByName() {
peopleRecords.sort((a, b) => {
const nameA =
getFieldValue(
a,
FIELD_IDS.name
);
const nameB =
getFieldValue(
b,
FIELD_IDS.name
);
const comparison =
nameA.localeCompare(nameB);
return nameSortDirection === "asc"
? comparison
: -comparison;
});
const sortButton =
document.getElementById("sortName");
if (nameSortDirection === "asc") {
sortButton.textContent =
"Name ↑";
nameSortDirection =
"desc";
} else {
sortButton.textContent =
"Name ↓";
nameSortDirection =
"asc";
}
renderRecords(
peopleRecords
);
}
/*
* ============================================================
* LESSON 3 — BUILD SEARCH CRITERIA
* ============================================================
*
* Unlike Lesson 2, we are NOT searching peopleRecords.
*
* We read the learner's search value and turn it into a
* Quickbase query expression.
*/
function searchByFavoriteColor() {
const input =
document.getElementById(
"favoriteColorSearch"
);
const color =
input.value.trim();
/*
* ==========================================================
* NO CRITERIA = NO QUICKBASE QUERY
* ==========================================================
*
* An empty search should not mean "return everything."
*
* Lesson 3 intentionally starts with no records and requires
* actual search criteria before querying Quickbase.
*/
if (!color) {
const status =
document.getElementById(
"status"
);
peopleRecords = [];
renderRecords(
peopleRecords
);
status.classList.remove(
"error"
);
status.textContent =
"Enter a Favorite Color before searching.";
return;
}
/*
* ==========================================================
* QUICKBASE WHERE EXPRESSION
* ==========================================================
*
* Favorite Color is Field ID 8.
*
* If the learner enters:
*
* Blue
*
* this becomes:
*
* {8.EX.'Blue'}
*
* EX means exact match.
*
* This expression is sent to Quickbase. The browser is not
* filtering an already-loaded dataset.
*/
const where =
`{${FIELD_IDS.favoriteColor}.EX.'${color}'}`;
/*
* Run a NEW Quickbase REST query using the criteria.
*/
loadPeople(where);
}
/*
* ============================================================
* LESSON 3 — QUERY QUICKBASE USING AN OPTIONAL WHERE CLAUSE
* ============================================================
*
* Lesson 1B introduced this REST records query.
*
* Lesson 3 adds one major capability:
*
* where
*
* The caller can now tell Quickbase which records should
* qualify before Quickbase returns the result set.
*/
async function loadPeople(where = "") {
const startTime =
performance.now();
const status =
document.getElementById(
"status"
);
try {
const temporaryToken =
await getTemporaryToken();
const query = {
from: TABLE_DBID,
/*
* ======================================================
* QUICKBASE QUERY FILTER
* ======================================================
*
* This is the new Lesson 3 property.
*
* Example:
*
* where: "{8.EX.'Blue'}"
*
* Quickbase evaluates this condition before returning
* records.
*/
where,
select: [
FIELD_IDS.recordId,
FIELD_IDS.name,
FIELD_IDS.age,
FIELD_IDS.favoriteColor,
],
options: {
top: 200,
skip: 0,
},
sortBy: [
{
fieldId:
FIELD_IDS.name,
order: "ASC",
},
],
};
const response = await fetch(
"https://api.quickbase.com/v1/records/query",
{
method: "POST",
headers: {
"QB-Realm-Hostname":
REALM,
Authorization:
`QB-TEMP-TOKEN ${temporaryToken}`,
"Content-Type":
"application/json",
},
body:
JSON.stringify(query),
},
);
const {
parsed,
rawBody,
} = await readQuickbaseResponse(
response
);
if (!response.ok) {
const details =
parsed
? JSON.stringify(parsed)
: rawBody;
throw new Error(
`HTTP ${response.status}: ${details}`
);
}
const records =
Array.isArray(parsed?.data)
? parsed.data
: [];
if (!Array.isArray(parsed?.data)) {
throw new Error(
"REST response did not include a data array."
);
}
if (records.length === 0) {
peopleRecords = [];
renderRecords(
peopleRecords
);
const endTime =
performance.now();
const renderTime =
endTime - startTime;
status.classList.remove(
"error"
);
status.textContent =
`REST query returned 0 record(s) in ${renderTime.toFixed(2)} ms.`;
return;
}
/*
* Keep the records returned by THIS search.
*
* Sorting from Lesson 2 can still operate on them afterward.
*/
peopleRecords =
records;
renderRecords(
peopleRecords
);
const endTime =
performance.now();
const renderTime =
endTime - startTime;
status.classList.remove(
"error"
);
status.textContent =
`REST query returned ${records.length} record(s) in ${renderTime.toFixed(2)} ms.`;
} catch (error) {
console.error(error);
status.classList.add(
"error"
);
status.textContent =
`REST query failed: ${error.message}`;
}
}
/*
* ============================================================
* IMPORTANT LESSON 3 CHANGE
* ============================================================
*
* Notice that loadPeople() is NOT called here.
*
* The page begins with an empty result table.
*
* Quickbase is queried only after the learner supplies search
* criteria and clicks Search Quickbase.
*/
document.addEventListener(
"DOMContentLoaded",
() => {
document
.getElementById(
"sortName"
)
.addEventListener(
"click",
sortByName
);
document
.getElementById(
"searchButton"
)
.addEventListener(
"click",
searchByFavoriteColor
);
}
);
</script>
</body>
</html>Lesson 3 Complete
You should now understand that a Quickbase REST query does not have to retrieve the entire available dataset.
A where expression can tell Quickbase which records qualify before the response is returned.
Lesson 2 gave us control after the data returned. Lesson 3 gives us control over what returns.